工程GIT地址:https://gitee.com/yaksue/yaksue-graphics
Shader代码所需要的资源该如何由应用程序来设置?
关于这个问题,我在《图形API学习工程(14):资源和描述符(Descriptors),资源如何绑定到管线上》 和《图形API学习工程(17):着色器中的资源绑定(Resource binding in Shader)》中做了些讨论和实验。我想是时候对相关的代码进行大刀阔斧地修改一下,让其更“正确”一些了。(即没有硬编码的数据或者是临时的假设)。
因此,本篇的目标是:
重构渲染管线在绑定资源方面的代码,使其:
为了让代码能更“正确”地重构,我需要想明白一些概念性问题:
由于:
所以:
因此,我准备在创建管线对象(Pipeline State Object)的时候,创建出资源布局的信息。
(唯一的问题是,不同的管线有可能其资源布局是相同的,所以这样设计可能会造成重复浪费)
我想全局只需要一个(实验发现是没问题的)。不过其所容纳的Descriptor数目,应该在开始分配一个较大值。(除非能知道应用程序最多需要多少个)
另外,对于D3D12的 DescriptorHeap,和Vulkan的 DescriptorPool,其行为有所区别。:
vkAllocateDescriptorSets
来从Descriptor池子中分配Descriptor的。由于:
因此,我选择将“设定资源”这一职责放到了管线对象(Pipeline State Object)上。这样的话,后期我还可以加上自己的“验证层”,来验证是否资源被设定绑定到了一个不存在的编号上。
正如之前所说,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。AllocateOneDescriptor()
,这样就可以根据AllocateCount
返回一个还没有使用的序号了。GetCPUHandle
和GetGPUHandle
来获得在CPU和GPU端的Handle。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是通过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);
}
//着色器资源的种类
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);
}
}
}
};
我希望当前场景里:
当前的管线和资源的初始化设置:
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();
}
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();
}
正如之前的博客所说,资源和描述符是不同的概念,一个资源可能对应于多个描述符。但现在工程里,描述符是在创建资源的时候同步创建的。如果未来出现一个资源有多个用途的情况,则代码需要改变。
Vulkan的和D3D12的Descriptor的概念似乎不一样。感觉Vulkan的资源在通往VkDescriptorSet
的路上还有一层封装,就比如从VkImage
到VkImageView
。
目前工程里关于sampler的代码还较乱,待研究如何统一封装。
目前工程里描述管线对象的时候,提供了着色器文件和资源的描述。但管线对象还包括其他方面的数据,这些数据还没有统一封装,待日后研究。
其实在这次提交之前,OpenGL显示都不正常:
现在显示正常,但代码中看起来有些奇怪:
一个变量被重复用了多次,甚至是在glUniformBlockBinding
函数中用了两次。
直觉告诉我,现在代码里应该有某种“巧合”,此方面待查。