图形API学习工程(18):渲染管线相关代码重构,实现绑定多个资源以及DrawCall间切换资源

工程GIT地址:https://gitee.com/yaksue/yaksue-graphics

目标

Shader代码所需要的资源该如何由应用程序来设置?
关于这个问题,我在《图形API学习工程(14):资源和描述符(Descriptors),资源如何绑定到管线上》 和《图形API学习工程(17):着色器中的资源绑定(Resource binding in Shader)》中做了些讨论和实验。我想是时候对相关的代码进行大刀阔斧地修改一下,让其更“正确”一些了。(即没有硬编码的数据或者是临时的假设)。

因此,本篇的目标是:
重构渲染管线绑定资源方面的代码,使其:

  • 可以绑定多个同种类的多个资源
  • 可以在DrawCall间切换所绑定的资源

概念性思考

为了让代码能更“正确”地重构,我需要想明白一些概念性问题:

思考1. 资源布局的信息应该何时创建?

由于:

  • 资源的布局是由着色器定义的,即一种着色器只能有一种资源布局。
  • 而一个管线只能有一种着色器

所以:

  • 管线只会对应一个资源布局。

因此,我准备在创建管线对象(Pipeline State Object)的时候,创建出资源布局的信息。
(唯一的问题是,不同的管线有可能其资源布局是相同的,所以这样设计可能会造成重复浪费)

思考2. Descriptor池/堆 有几个?

我想全局只需要一个(实验发现是没问题的)。不过其所容纳的Descriptor数目,应该在开始分配一个较大值。(除非能知道应用程序最多需要多少个)

另外,对于D3D12的 DescriptorHeap,和Vulkan的 DescriptorPool,其行为有所区别。:

  • 对于Vulkan,是通过vkAllocateDescriptorSets来从Descriptor池子中分配Descriptor的。
  • 而D3D11分配Descriptor的过程需要相对较多的手动操控。

思考3. 资源应该由谁来设置?

由于:

  • 设定资源时需要指定绑定的编号
  • 而这个信息是是被着色器代码定义的,在创建管线对象时候指定的。

因此,我选择将“设定资源”这一职责放到了管线对象(Pipeline State Object)上。这样的话,后期我还可以加上自己的“验证层”,来验证是否资源被设定绑定到了一个不存在的编号上。

一些封装

D3D12 的 DescriptorHeap

正如之前所说,D3D12分配Descriptor的过程相对需要手动操控,不方便,因此我决定对此做些封装:

//对原先描述符堆的一层封装
class D3D12DescriptorHeapWrapper
{
     
    ComPtr<ID3D12DescriptorHeap> Heap;
    
    //一个Descriptor所占的尺寸
    UINT8 IncrementSize;

    //已经分配的Descriptor个数
    unsigned int AllocateCount;

    //总个数 
    unsigned int DescriptorAmount;
public:
    D3D12DescriptorHeapWrapper(D3D12_DESCRIPTOR_HEAP_TYPE Type, ComPtr<ID3D12Device> Device)
    {
     
        AllocateCount = 0;
        
        DescriptorAmount = 128;//给一个应该较大的值

        //创建Heap
        {
     
            D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {
     };
            rtvHeapDesc.NumDescriptors = DescriptorAmount;
            rtvHeapDesc.Type = Type;
            if (Type == D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV)
                rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
            else
                rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
            ThrowIfFailed(Device->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&Heap)));
        }    
        
        //获得Descriptor的尺寸
        IncrementSize = Device->GetDescriptorHandleIncrementSize(Type);
    }

    ID3D12DescriptorHeap* GetRawPointer() {
      return Heap.Get(); }

    //分配一个Descriptor,返回序号
    unsigned int AllocateOneDescriptor()
    {
     
        if (AllocateCount < DescriptorAmount)
        {
     
            AllocateCount++;
            return (AllocateCount - 1);
        }
        else
            return -1;
    }

    //得到一个Descriptor序号对应的CPUHandle
    D3D12_CPU_DESCRIPTOR_HANDLE GetCPUHandle(int DescriptorIndex)
    {
     
        CD3DX12_CPU_DESCRIPTOR_HANDLE handle(Heap->GetCPUDescriptorHandleForHeapStart());

        return handle.Offset(DescriptorIndex, IncrementSize);
    }
    //得到一个Descriptor序号对应的GPUHandle
    D3D12_GPU_DESCRIPTOR_HANDLE GetGPUHandle(int DescriptorIndex)
    {
     
        CD3DX12_GPU_DESCRIPTOR_HANDLE handle(Heap->GetGPUDescriptorHandleForHeapStart());

        return handle.Offset(DescriptorIndex, IncrementSize);
    }
};

其中:

  • 成员变量AllocateCount记录了当前分配了几个Descriptor。
  • 当需要一个新的Descriptor时,调用AllocateOneDescriptor(),这样就可以根据AllocateCount返回一个还没有使用的序号了。
  • 这个序号应被记录下来,随后将其作为参数可以调用GetCPUHandleGetGPUHandle来获得在CPU和GPU端的Handle。

Vulkan 的 DescriptorSet

Vulkan中的DescriptorSet是由vkAllocateDescriptorSets创建的,而它将管线的资源布局作为了一个参数。
所以在我看来,Vulkan中的DescriptorSet强迫将所有资源绑定为了一个组合,因此当管线上的任一资源有所变化时,都需要一个新的DescriptorSet。(此说法待查证)

因此,现阶段我决定让管线来维护一个map,其中存储着所有“资源的组合”对应的DescriptorSet:

//一组资源的组合对应一个VkDescriptorSet
std::map<std::vector<CommonShaderResource*>, VkDescriptorSet>DescriptorSetMaps;

而在绑定资源时,我将检查看当前的“资源的组合”是否已经由对应的DescriptorSet了,如果有则直接设置它,如果没有则创建一个新的:

void VulkanInterface::CmdBindPipelineCurrentDescriptorSets(CommonPipelineStateObject* PSO)
{
     
	Vulkan_GraphicsPipeline* vkPipeline = (Vulkan_GraphicsPipeline*)PSO;

	if (vkPipeline->DescriptorSetMaps.find(vkPipeline->CurrentShaderResources) == vkPipeline->DescriptorSetMaps.end())//没找到
	{
     		
		VkDescriptorSet DescriptorSet;

		//创建一个新的DescriptorSet
		{
     
			VkDescriptorSetLayout layouts[] = {
      vkPipeline->DescriptorSetLayout };
			VkDescriptorSetAllocateInfo allocInfo = {
     };
			allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
			allocInfo.descriptorPool = DescriptorPool;
			allocInfo.descriptorSetCount = 1;
			allocInfo.pSetLayouts = layouts;

			ThrowIfFailed(vkAllocateDescriptorSets(Device, &allocInfo, &DescriptorSet));
		}
		
		//遍历所有资源写入Descriptor
		for (int ResourceIndex = 0; ResourceIndex < vkPipeline->GetCommonInfo()->ShaderResourceSlots.size(); ResourceIndex++)
		{
     
			if (vkPipeline->CurrentShaderResources[ResourceIndex] == nullptr)//如果这个槽位还没有资源则跳过
				continue;

			auto res = vkPipeline->CurrentShaderResources[ResourceIndex];
			auto slot = vkPipeline->GetCommonInfo()->ShaderResourceSlots[ResourceIndex];

			VkWriteDescriptorSet WriteDescriptorSet = {
     };
			WriteDescriptorSet.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
			WriteDescriptorSet.dstSet = DescriptorSet;
			WriteDescriptorSet.dstBinding = slot.BindingIndex;
			WriteDescriptorSet.dstArrayElement = 0;
			WriteDescriptorSet.descriptorCount = 1;

			if (slot.Type == ShaderResourceType::UniformBuffer)
			{
     
				Vulkan_UniformBuffer* ub = (Vulkan_UniformBuffer*)res;

				VkDescriptorBufferInfo bufferInfo;
				bufferInfo.buffer = ub->uniformBuffers;
				bufferInfo.offset = 0;
				bufferInfo.range = ub->BufferSize;

				WriteDescriptorSet.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
				WriteDescriptorSet.pBufferInfo = &bufferInfo;
			}
			else if (slot.Type == ShaderResourceType::Texture)
			{
     
				Vulkan_TextureData* result = (Vulkan_TextureData*)res;

				VkDescriptorImageInfo imageInfo ;		
				imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
				imageInfo.imageView = result->textureImageView;
				imageInfo.sampler = result->textureSampler;

				WriteDescriptorSet.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
				WriteDescriptorSet.pImageInfo = &imageInfo;
			}

			//更新
			vkUpdateDescriptorSets(Device, 1,  &WriteDescriptorSet , 0, nullptr);
		}	

		vkPipeline->DescriptorSetMaps[vkPipeline->CurrentShaderResources] = DescriptorSet;
	}

	//将Descriptor绑定到管线上
	vkCmdBindDescriptorSets(CommandBuffers[CurrentCommandListIndex], VK_PIPELINE_BIND_POINT_GRAPHICS, vkPipeline->PipelineLayout, 0, 1
		, &vkPipeline->DescriptorSetMaps[vkPipeline->CurrentShaderResources], 0, nullptr);
}

OpenGL 的 UniformBlockIndex

正如在之前的博客所讨论的,OpenGL是通过UniformBuffer的名字来找到一个所谓的“序号”的。

因此,对于OpenGL的管线对象,我将记录一个从名字对应到序号的map:

//UniformBuffer在shader中的序号对应
std::map<std::string, GLuint> UniformBlockIndexMappings;

在创建管线对象的时候完成这一信息的获取:

for (auto slot : result->GetCommonInfo()->ShaderResourceSlots)
{
     
    //记录UniformBuffer名字对应的序号
    if (slot.Type == ShaderResourceType::UniformBuffer)
    {
     
        //通过名字知道这个UniformBuffer在Shader中的序号
        result->UniformBlockIndexMappings[slot.Name] = glGetUniformBlockIndex(result->Program, slot.Name.c_str());
    }
}

这样在设置的时候就可以查询这个map了:

if (Slot.Type == ShaderResourceType::UniformBuffer)
{
     
    OpenGL_UniformBuffer* ub = (OpenGL_UniformBuffer*)InResource;

    GLuint UniformBlockIndex = UniformBlockIndexMappings[Slot.Name];

    //绑定这个UniformBuffer在Shader中的序号
    glUniformBlockBinding(Program, UniformBlockIndex, UniformBlockIndex);
    glBindBufferBase(GL_UNIFORM_BUFFER, UniformBlockIndex, ub->BufferHandle);
}

当前的管线对象(Pipeline State Object)

//着色器资源的种类
namespace ShaderResourceType
{
     
	enum Type
	{
     
		UniformBuffer,
		Texture,
		Sampler,
	};
}

//代表一个着色器资源,比如UniformBuffer,或者纹理
class CommonShaderResource
{
     
};

//一个着色器资源的槽位信息
struct ShaderResourceSlotInfo
{
     
	ShaderResourceType::Type Type;	//资源种类
	std::string Name;				//资源名字
	unsigned int BindingIndex;		//绑定的序号
	ShaderResourceSlotInfo(ShaderResourceType::Type InType, std::string InName, unsigned int InBindingIndex)
	{
     
		Type = InType;
		Name = InName;
		BindingIndex = InBindingIndex;
	}
};

//描述一个图形管线的信息
struct GraphicsPipelineInfo
{
     
	std::string VertexShaderFile;	//顶点着色器的文件名
	std::string PixelShaderFile;	//像素着色器的文件名
	std::vector<VertexInputAttributeType> VertexInputAttributeTypes;//顶点输入属性的种类
	std::vector<ShaderResourceSlotInfo> ShaderResourceSlots;	//着色器资源的槽位,即描述符布局
};

//公共的管线状态对象,不同的图形API应对其有不同的继承并加入其需要的成员
class CommonPipelineStateObject
{
     
protected:
	//创建时提供的公共信息
	GraphicsPipelineInfo CommonInfo;

public:
	//顶点输入布局
	std::vector<VertexInputAttributeDescription> VertexInputAttributes;

	//当前的资源,对应于ShaderResourceSlots
	std::vector<CommonShaderResource*> CurrentShaderResources;


	//构造函数
	CommonPipelineStateObject(GraphicsPipelineInfo Info)
	{
     
		CommonInfo = Info;

		//自动设置顶点输入属性的描述
		for (int i = 0; i < Info.VertexInputAttributeTypes.size(); i++)
			VertexInputAttributes.push_back(VertexInputAttributeDescription(Info.VertexInputAttributeTypes[i]));

		//当前的资源初始为空
		for (int i = 0; i < Info.ShaderResourceSlots.size(); i++)
			CurrentShaderResources.push_back(nullptr);
	}

	const GraphicsPipelineInfo* GetCommonInfo() {
      return &CommonInfo; }

protected: //内部调用

	//设定资源(子类应该继承它)
	//对于传统图形API,是直接将资源绑定到渲染管线上了
	//对于先进图形API,只是告诉管线其所需的资源是什么,真正的绑定需要在之后使用【命令】
	virtual void InternelSetResource(unsigned int ResourceIndex, CommonShaderResource* InResource)
	{
     
		CurrentShaderResources[ResourceIndex] = InResource;
	}

public: //外部接口

	//通过名字设定资源
	void SetResourceByName(std::string ResourceName, CommonShaderResource* InResource)
	{
     
		for (int i = 0; i < CommonInfo.ShaderResourceSlots.size(); i++)
		{
     
			if (CommonInfo.ShaderResourceSlots[i].Name == ResourceName)
			{
     
				InternelSetResource(i, InResource);
			}
		}
	}
	//通过绑定号设定资源
	void SetResourceByBindingIndex(ShaderResourceType::Type Type, unsigned int BindingIndex, CommonShaderResource* InResource)
	{
     
		for (int i = 0; i < CommonInfo.ShaderResourceSlots.size(); i++)
		{
     
			if ((CommonInfo.ShaderResourceSlots[i].Type == Type)&&
				(CommonInfo.ShaderResourceSlots[i].BindingIndex == BindingIndex))
			{
     
				InternelSetResource(i, InResource);
			}
		}
	}
};

测试场景

我希望当前场景里:

  • 画两个图形所用的贴图不一样。
  • 金字塔模型的顶点数据坐标也在中心,用UniformBuffer的矩阵来设置位置。

当前的管线和资源的初始化设置:

void Renderer::Init(GraphicsInterface* IngraphicsAPI, GLFWwindow* window)
{
     
	graphicsAPI = IngraphicsAPI;
	graphicsAPI->Init(window);

	//创建作为测试的管线对象
	{
     
		GraphicsPipelineInfo Info;
		//shader文件:
		Info.VertexShaderFile = "TestShader_vs";
		Info.PixelShaderFile = "TestShader_ps";
		//顶点属性:
		Info.VertexInputAttributeTypes.push_back(VIA_POSITION);	//位置
		Info.VertexInputAttributeTypes.push_back(VIA_NORMAL);	//法线
		Info.VertexInputAttributeTypes.push_back(VIA_TEXCOORD);	//贴图UV
		//资源布局:
		Info.ShaderResourceSlots.push_back(ShaderResourceSlotInfo(ShaderResourceType::UniformBuffer, "ModelData", 6));
		Info.ShaderResourceSlots.push_back(ShaderResourceSlotInfo(ShaderResourceType::UniformBuffer, "SceneData", 7));
		Info.ShaderResourceSlots.push_back(ShaderResourceSlotInfo(ShaderResourceType::Texture, "ourTexture", 9));
		Info.ShaderResourceSlots.push_back(ShaderResourceSlotInfo(ShaderResourceType::Sampler, "ourSampler", 5));

		//创建图形管线对象
		MyTestGraphicsPipeline = graphicsAPI->CreateGraphicsPipeline(Info);
	}
	

	//测试方块
	TestCubeMesh = new TempTestCube();
	//创建方块的顶点缓冲
	TestCubeMesh->GenerateVertexBufferForPipeline(MyTestGraphicsPipeline, graphicsAPI);
	//测试金字塔
	TestPyramidsMesh = new TempTestPyramids();
	//创建金字塔的顶点缓冲
	TestPyramidsMesh->GenerateVertexBufferForPipeline(MyTestGraphicsPipeline, graphicsAPI);
	
	//创建UniformBuffer信息:
	{
     
		UniformBufferCreateInfo UBInfo;

		//场景的数据:
		UBInfo.BufferAddress = &data_scene;
		UBInfo.BufferSize = sizeof(SceneData);
		ub_scene = graphicsAPI->CreateUniformBuffer(UBInfo);

		//第一个模型的信息(transform信息):
		UBInfo.BufferAddress = &data_model1;
		UBInfo.BufferSize = sizeof(ModelData);
		ub_model1 = graphicsAPI->CreateUniformBuffer(UBInfo);

		//第二个模型的信息(transform信息):
		UBInfo.BufferAddress = &data_model2;
		UBInfo.BufferSize = sizeof(ModelData);
		ub_model2 = graphicsAPI->CreateUniformBuffer(UBInfo);
	}
	
	//创建贴图:
	MyTestTexture = graphicsAPI->CreateTexture("textures/Brick_Medieval_albedo.png");
	MyTestTexture2 = graphicsAPI->CreateTexture("textures/BrickRound0109.png");
}

StandardRenderer(OpenGL和D3D11):

void StandardRenderer::Render()
{
     
	graphicsAPI_standard->Clear(0.4f, 0.5f, 0.6f, 1.0f);

	//设置图形管线
	graphicsAPI->SetGraphicsPipeline(MyTestGraphicsPipeline);
	
	//更新UniformBuffer:
	graphicsAPI->UpdateUniformBuffer(ub_scene);
	graphicsAPI->UpdateUniformBuffer(ub_model1);
	graphicsAPI->UpdateUniformBuffer(ub_model2);


	//设定Sampler:
	MyTestGraphicsPipeline->SetResourceByName("ourSampler", MyTestTexture);
	//设定场景相关的UniformBuffer
	MyTestGraphicsPipeline->SetResourceByName("SceneData", ub_scene);

	//画方块
	{
     
		//方块的贴图和transform
		MyTestGraphicsPipeline->SetResourceByName("ourTexture", MyTestTexture);
		MyTestGraphicsPipeline->SetResourceByName("ModelData", ub_model1);

		//设置顶点缓冲
		graphicsAPI->SetVertexBuffer(TestCubeMesh->GetVertexBuffer(MyTestGraphicsPipeline));
		//绘制
		graphicsAPI_standard->DrawIndexed(TestCubeMesh->GetVertexBuffer(MyTestGraphicsPipeline)->IndexCount, 0, 0);
	}

	//画金字塔
	{
     
		//金字塔的贴图和transform
		MyTestGraphicsPipeline->SetResourceByName("ourTexture", MyTestTexture2);
		MyTestGraphicsPipeline->SetResourceByName("ModelData", ub_model2);	

		//设置顶点缓冲
		graphicsAPI->SetVertexBuffer(TestPyramidsMesh->GetVertexBuffer(MyTestGraphicsPipeline));
		//绘制
		graphicsAPI_standard->DrawIndexed(TestPyramidsMesh->GetVertexBuffer(MyTestGraphicsPipeline)->IndexCount, 0, 0);
	}

	graphicsAPI->Present();
}

图形API学习工程(18):渲染管线相关代码重构,实现绑定多个资源以及DrawCall间切换资源_第1张图片
图形API学习工程(18):渲染管线相关代码重构,实现绑定多个资源以及DrawCall间切换资源_第2张图片


AdvancedRenderer(D3D12和Vulkan):

void AdvancedRenderer::Render()
{
     
	//更新UniformBuffer:
	graphicsAPI->UpdateUniformBuffer(ub_scene);
	graphicsAPI->UpdateUniformBuffer(ub_model1);
	graphicsAPI->UpdateUniformBuffer(ub_model2);

	for (int CommandListIndex = 0; CommandListIndex < graphicsAPI_Advanced->QueryCommandListCount(); CommandListIndex++)
	{
     
		graphicsAPI_Advanced->CurrentCommandListIndex = CommandListIndex;

		graphicsAPI_Advanced->BeginRecordCommandList();//开始录制命令
		{
     
			//开始RenderPass
			float ClearColor[4] = {
      0.6f, 0.1f, 0.1f, 1.0f };
			graphicsAPI_Advanced->BeginRenderPass(ClearColor);

			//设置图形管线
			graphicsAPI->SetGraphicsPipeline(MyTestGraphicsPipeline);				

			//设定场景相关的UniformBuffer
			MyTestGraphicsPipeline->SetResourceByName("SceneData", ub_scene);
		
			//画方块
			{
     
				//方块的贴图和transform
				MyTestGraphicsPipeline->SetResourceByName("ourTexture", MyTestTexture);
				MyTestGraphicsPipeline->SetResourceByName("ModelData", ub_model1);
				//绑定当前的资源
				graphicsAPI_Advanced->CmdBindPipelineCurrentDescriptorSets(MyTestGraphicsPipeline);

				//设置顶点缓冲
				graphicsAPI->SetVertexBuffer(TestCubeMesh->GetVertexBuffer(MyTestGraphicsPipeline));
				//绘制
				graphicsAPI_Advanced->CmdDrawIndexedInstanced(TestCubeMesh->GetVertexBuffer(MyTestGraphicsPipeline)->IndexCount, 1, 0, 0, 0);
			}

			//画金字塔
			{
     
				//金字塔的贴图和transform
				MyTestGraphicsPipeline->SetResourceByName("ourTexture", MyTestTexture2);
				MyTestGraphicsPipeline->SetResourceByName("ModelData", ub_model2);
				//绑定当前的资源
				graphicsAPI_Advanced->CmdBindPipelineCurrentDescriptorSets(MyTestGraphicsPipeline);

				//设置顶点缓冲
				graphicsAPI->SetVertexBuffer(TestPyramidsMesh->GetVertexBuffer(MyTestGraphicsPipeline));
				//绘制
				graphicsAPI_Advanced->CmdDrawIndexedInstanced(TestPyramidsMesh->GetVertexBuffer(MyTestGraphicsPipeline)->IndexCount, 1, 0, 0, 0);
			}

			//结束Renderpass
			graphicsAPI_Advanced->EndRenderPass();
		}
		graphicsAPI_Advanced->EndRecordCommandList();//结束录制命令
	}

	//执行命令
	graphicsAPI_Advanced->ExecuteCommandLists();

	graphicsAPI->Present();
}

图形API学习工程(18):渲染管线相关代码重构,实现绑定多个资源以及DrawCall间切换资源_第3张图片
图形API学习工程(18):渲染管线相关代码重构,实现绑定多个资源以及DrawCall间切换资源_第4张图片

遗留问题

问题1. 资源和Descriptor

正如之前的博客所说,资源和描述符是不同的概念,一个资源可能对应于多个描述符。但现在工程里,描述符是在创建资源的时候同步创建的。如果未来出现一个资源有多个用途的情况,则代码需要改变。

问题2. Vulkan的和D3D12的Descriptor

Vulkan的和D3D12的Descriptor的概念似乎不一样。感觉Vulkan的资源在通往VkDescriptorSet的路上还有一层封装,就比如从VkImageVkImageView

问题3. sampler

目前工程里关于sampler的代码还较乱,待研究如何统一封装。

问题4. 描述管线对象

目前工程里描述管线对象的时候,提供了着色器文件和资源的描述。但管线对象还包括其他方面的数据,这些数据还没有统一封装,待日后研究。

问题5. OpenGL的UniformBlockIndex

其实在这次提交之前,OpenGL显示都不正常:
在这里插入图片描述
现在显示正常,但代码中看起来有些奇怪:
图形API学习工程(18):渲染管线相关代码重构,实现绑定多个资源以及DrawCall间切换资源_第5张图片
一个变量被重复用了多次,甚至是在glUniformBlockBinding函数中用了两次。
直觉告诉我,现在代码里应该有某种“巧合”,此方面待查。

你可能感兴趣的:(图形API,opengl,shader,vulkan,directx,direct3d)