0.关键代码
几何数据需要按照一定的布局从CPU传入GPU:
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 },
};
传递数据通过以下代码实现:
Microsoft::WRL::ComPtr<ID3D12Resource> VertexBufferGPU = nullptr;
Microsoft::WRL::ComPtr<ID3D12Resource> VertexBufferUploader = nullptr;
//将数据传递到VertexBufferGPU
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);
将CPU定义好的顶点数据vertices.data(),通过cmdList->IASetVertexBuffers(0, 1, &vbv);这把钥匙放进了GPU,本身IA开头的函数就意味着输入装配InputAssemble。
1.资源定义
Microsoft::WRL::ComPtr<ID3D12Resource> VertexBufferGPU = nullptr;
Microsoft::WRL::ComPtr<ID3D12Resource> VertexBufferUploader = nullptr;
ID3D12Resource这个类代表了GPU资源,RTV和DSV,也是这个类型,D3D12对这个进行了统一,其实在DX11中还是区分什么ID3D11Buffer和ID3D11Texture2D这样的,那么DX12既然统一了,那怎么来区分呢?接着往下看。
2.数据传输的渠道
VertexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(), mCommandList.Get(), vertices.data(), vbByteSize,VertexBufferUploader);
对应上边的资源定义,一个是左值,另一个是最后一个参数,对应默认堆和上传堆,VertexBufferGPU实际是个默认堆,而VertexBufferUploader实际是个上传堆:
先看一下整体代码:
Microsoft::WRL::ComPtr<ID3D12Resource> d3dUtil::CreateDefaultBuffer(
ID3D12Device* device,
ID3D12GraphicsCommandList* cmdList,
const void* initData,
UINT64 byteSize,
Microsoft::WRL::ComPtr<ID3D12Resource>& uploadBuffer)
{
ComPtr<ID3D12Resource> defaultBuffer;
// 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())));
// Describe the data we want to copy into the default buffer.
D3D12_SUBRESOURCE_DATA subResourceData = {};
subResourceData.pData = initData;
subResourceData.RowPitch = byteSize;
subResourceData.SlicePitch = subResourceData.RowPitch;
// Schedule to copy the data to the default buffer resource. At a high level, the helper function UpdateSubresources
// will copy the CPU memory into the intermediate upload heap. Then, using ID3D12CommandList::CopySubresourceRegion,
// the intermediate upload heap data will be copied to mBuffer.
cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(defaultBuffer.Get(),
D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_COPY_DEST));
UpdateSubresources<1>(cmdList, defaultBuffer.Get(), uploadBuffer.Get(), 0, 0, 1, &subResourceData);
cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(defaultBuffer.Get(),
D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_GENERIC_READ));
// Note: uploadBuffer has to be kept alive after the above function calls because
// the command list has not been executed yet that performs the actual copy.
// The caller can Release the uploadBuffer after it knows the copy has been executed.
return defaultBuffer;
}
// 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传导上传堆,再到默认堆
// <1>是模板参数,表示Subresource的数量
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));
现在数据已经被我们送到GPU了,但是还没通知到输入装配。
3.通知输入装配
通知时大概的对话是这样的:
我们:货已经到了,放在了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);
也就是说一个槽上边可以放多个信息,只要你不一次性放进去(意味着通知当时拿出当时数据,下次通知会更改数据)。
一般而言我们绘图都是根据索引来的,类似顶点缓冲区传递数据。
4.索引缓冲区
Microsoft::WRL::ComPtr<ID3D12Resource> IndexBufferGPU = nullptr;
Microsoft::WRL::ComPtr<ID3D12Resource> IndexBufferUploader = 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);
都准备好了,DrawCall!!!
5.DrawCall
这里我们根据顶点缓冲和索引缓冲数据使用函数:
cmdList->DrawIndexedInstanced(ri->IndexCount, 1, ri->StartIndexLocation, ri->BaseVertexLocation, 0);
同样是加入命令队列,首先告诉他我们每IndexCount个索引画一个Mesh,从索引堆里边第StartIndexLocation开始看,从顶点堆里第BaseVertexLocation开始看,最后一个桉树表示是否使用实例化。
至此,驱动单个像素生成的流水线就启动了。