首先声明,本人是自学DX12,有很多的理解也许不到位,不过都是自己的理解。在很长一段时间里边,我从迷茫到有一天开始能看懂,现在是第三次开始刷DX12了,于是在此表明写作的初衷:
1.有一些DX12的学习心得,希望发出来,有大佬如果愿意指教,万分感谢;
2.如果对于才入门的人来说,这可能是我的白话教程,也许会对你有所帮助,但不可尽信,因为我也不确定我对不对;
3.DX12的概念很多,也是想把这作为自己的学习笔记来做,希望对自己也有帮助,如果有一天我发现哪里错了会及时回来更正。
那么话不多说,现在开始!!!
在传数据之前,我们要先生产出数据(C++的逻辑我们就略了)并把数据打包:
std::vector<D3D12_INPUT_ELEMENT_DESC> mInputLayout;
mInputLayout =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
};
//语义,语义索引,格式,槽,偏移值,数据类型,是否实例化
好了,开始传输数据!
上一篇我们讲了渲染流水线,开头就是分拣员输入装配(Input assembly)阶段,但我们没有关心CPU是如何向GPU传输数据的。如果这个处理工厂是GPU,那么今天就来看看CPU是造了一辆什么样的车给GPU传了数据。
今天换个模式,先看看我们今天要分析的这样一串代码:
Microsoft::WRL::ComPtr<ID3D12Resource> VertexBufferGPU = nullptr;
Microsoft::WRL::ComPtr<ID3D12Resource> VertexBufferUploader = nullptr;
VertexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
mCommandList.Get(), vertices.data(), vbByteSize, VertexBufferUploader);
D3D12_VERTEX_BUFFER_VIEW vbv;
vbv.BufferLocation = VertexBufferGPU->GetGPUVirtualAddress();
vbv.StrideInBytes = VertexByteStride;
vbv.SizeInBytes = VertexBufferByteSize;
cmdList->IASetVertexBuffers(0, 1, &vbv);
是的,如果只是在DX12里边利用封装好的函数,我们就已经实现了将CPU定义好的顶点数据vertices.data(),通过cmdList->IASetVertexBuffers(0, 1, &vbv);这把钥匙放进了GPU,本身IA开头的函数就意味着输入装配。
如果你是个Code Copyer后边就不用看了,毕竟上边就可以到达流水线的顶端了。
OK,我还是比较感兴趣这是怎么做到的,那就一层一层的剥开他的心:
一、资源定义
Microsoft::WRL::ComPtr<ID3D12Resource> VertexBufferGPU = nullptr;
Microsoft::WRL::ComPtr<ID3D12Resource> VertexBufferUploader = nullptr;
ID3D12Resource这个类代表了GPU资源,还记得我们的那些个RTV和DSV吗,也是这个类型。这是因为D3D12对这个进行了统一,其实在DX11中还是区分什么ID3D11Buffer和ID3D11Texture2D这样的,那么DX12既然统一了,那怎么来区分呢?接着往下看。
二、数据传输的渠道
VertexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
mCommandList.Get(), vertices.data(), vbByteSize,VertexBufferUploader);
这个函数似乎看不出数据是怎么传输的,不妨看看这个左值以及最后一个参数,如果认真看了D3D的初始化部分,在创建DSV的时候提到过资源的四种类型,重点关注默认堆和上传堆,这里就是了:VertexBufferGPU实际是个默认堆,而VertexBufferUploader实际是个上传堆。一起来看看函数里边做了啥:
// Create the actual default buffer resource.
ThrowIfFailed(device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(byteSize),
D3D12_RESOURCE_STATE_COMMON,
nullptr,
IID_PPV_ARGS(defaultBuffer.GetAddressOf())));
// In order to copy CPU memory data into our default buffer, we need to create
// an intermediate upload heap.
ThrowIfFailed(device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(byteSize),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(uploadBuffer.GetAddressOf())));
熟悉的函数CreateCommittedResource,创建一个堆和一个资源,根据类型的不同就是默认堆和上传堆。注意到默认堆创建的时候的访问权限Common,我们现在是要往默认堆上传数据,也就是写入,因此进行资源转换:
cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(defaultBuffer.Get(),
D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_COPY_DEST));
这也是一个我们说过的函数。好了,存储空间,读写权限都好了,那就是准备数据:
D3D12_SUBRESOURCE_DATA subResourceData = {};
subResourceData.pData = initData;//数据本身
subResourceData.RowPitch = byteSize;//描述数据的大小,占用内存
subResourceData.SlicePitch = subResourceData.RowPitch;//描述数据的大小,占用内存
万事俱备,只欠东风:
//核心函数,将数据subResourceData从CPU传导上传堆,再到默认堆
UpdateSubresources<1>(cmdList, defaultBuffer.Get(), uploadBuffer.Get(), 0, 0, 1, &subResourceData);
注意到这个cmdlist了吗,这就表明我们实际是将传数据放在命令列表,交由命令队列来做的,这里有数据及其描述,也有中间过渡的上传堆uploadBuffer,还有传入终点defaultBuffer。
做完之后别忘了,现在的默认堆是支持写入的,后边获取数据实际是读出:
cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(defaultBuffer.Get(),
D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_GENERIC_READ));
最后把这个默认堆返回:return defaultBuffer;
实际上,这个时候,我们可以这么认为,数据已经被我们送到GPU了,但是是默默的送到了GPU,被GPU工厂的一个叫做默认堆的仓库存起来了,可是输入装配这个分拣员他不知道啊,于是乎我们去找他谈谈。
三、通知输入装配
谈话之前我们要详细做个规划,我们要讲点什么?大概的对话是这样的:
我们:货已经到了,放在了XXXX,它大概这么大,你要每隔多少米切一下才完整。所以我们列出了以下关键词:
D3D12_VERTEX_BUFFER_VIEW vbv;
vbv.BufferLocation = VertexBufferGPU->GetGPUVirtualAddress();//获得GPU存储位置地址
vbv.SizeInBytes = VertexBufferByteSize;
vbv.StrideInBytes = VertexByteStride;
然后,将我们的这个命令又安排进队列等待执行:
cmdList->IASetVertexBuffers(0, 1, &vbv);
注意这个函数的定义:
IASetVertexBuffers(
_In_ UINT StartSlot,//起始槽编号,0~15
_In_ UINT NumViews,//顶点缓冲区数量,描述第三个参数数组有几个顶点缓冲,要注意一个槽对应一个缓冲区,所以也就是说第三个参数最多不能超过16
_In_reads_opt_(NumViews) const D3D12_VERTEX_BUFFER_VIEW *pViews)
看到这里,我们就把CPU的数据送到了GPU的存储位置上,并且告知了输入装配阶段,老规矩,注意事项:
实际上如果有多个顶点缓冲区View1和View2,我们可以用下边的操作:
cmdList->IASetVertexBuffers(0, 1, &View1);
cmdList->IASetVertexBuffers(0, 1, &View2);
也就是说一个槽上边可以放多个信息,只要你不一次性放进去(第二个参数不为1)。
到目前为止,我们只是把数据放进了GPU里边,通知了输入装配,你以为分拣员就会理你吗?对了,你要push他,发出DrawCall的指令。一般而言我们绘图都是根据索引来的,所以暂且不表,先说索引缓冲区。
四、索引缓冲区
具体意思不解释了,自行百度,优秀的人都会百度——语出某高校2级教授:)
Microsoft::WRL::ComPtr<ID3DBlob> IndexBufferCPU = nullptr;
Microsoft::WRL::ComPtr<ID3D12Resource> IndexBufferGPU = nullptr;
IndexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
mCommandList.Get(), indices.data(), ibByteSize, IndexBufferUploader);
D3D12_INDEX_BUFFER_VIEW ibv;
ibv.BufferLocation = IndexBufferGPU->GetGPUVirtualAddress();//获得GPU存储位置地址
ibv.Format = IndexFormat;//描述格式,因为sizeof就能得到间隔了,其实和上边原理一样
ibv.SizeInBytes = IndexBufferByteSize;
cmdList->IASetIndexBuffer(&ibv);
看到这里,你可以对照开头给出的那段顶点缓冲的代码,不能说毫不相关吧,简直是一模一样。
索引缓冲也设置好了,插播一条新闻,我们奖分拣员的时候说了还要告诉他是用什么图元拓扑关系来指导他操作,他来了:
cmdList->IASetPrimitiveTopology(PrimitiveType);
都准备好了,开始Push流水线的员工吧,我们要给分拣员说:赶紧的,工资不白发!!!
五、DrawCall
这里我们根据顶点缓冲和索引缓冲数据使用函数:
cmdList->DrawIndexedInstanced(ri->IndexCount, 1, ri->StartIndexLocation, ri->BaseVertexLocation, 0);
同样是加入命令队列,首先告诉他我们每IndexCount个索引画一个Mesh,从索引堆里边第StartIndexLocation开始看,从顶点堆里第BaseVertexLocation开始看,不要使用实例化这种花里胡哨的操作。
所以后边咋做,就是我们要给流水线上的员工发作案工具了,这就是shader的讨论范围了。我们在后边也会分析一些shader。