龙书最简单的shader代码定义了一个数据:
cbuffer cbPerObject : register(b0)
{
float4x4 gWorld; //用于shader使用的计算资源
};
cbuffer:常量缓冲区
cbPerObject:对象名
register:寄存器
(b0):b类寄存器上的0#槽。
我们关心两个问题:1.常量缓冲器怎么回事?2.寄存器的槽怎么设置?
一、常量缓冲区
既然是缓冲区Buffer,那也是着色器的一种资源(ID3D12Resource),是一种把数据从CPU运输到GPU的方式。
对于模型顶点这类数据,他可以认为在模型空间中是稳定的,不变的,在流水线上只用对其进行坐标变换就可以得到不同的观察效果,所以直接放入默认堆,后边CPU不再更新这部分数据,也不再让GPU更改他。但是有一部分数据,比如我们改了摄像机的视角,那总要告诉渲染流水线对我们的模型顶点数据进行变换,所以常量缓冲区就诞生了——他是一个每一帧都要更新的缓冲区。为了实现每一帧都要更新——使用上传堆。
注意,常量缓冲区中的数据必须是256B的整数倍(256B),是硬件存储最小分配空间。我们可以自己设置,也可以程序帮我们实现。
return (byteSize + 255) & ~255;//结果就是256B整数倍。
1.整体流程
先是进行数据的准备:CPU端定义待传递数据的基本结构:
struct ObjectConstants
{
DirectX::XMFLOAT4X4 World = MathHelper::Identity4x4();
};
建立上传堆并存储数据
因为数据要实现每帧更新,就要用到上传堆,在传输顶点数据的时候我们看到过,现在再次贴出来:
ThrowIfFailed(device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(mElementByteSize*elementCount),//空间大小
D3D12_RESOURCE_STATE_GENERIC_READ,//资源读写权限
nullptr,//资源的清除格式
IID_PPV_ARGS(&mUploadBuffer)));
这个函数创建了一个上传堆,产生了存储数据的一组内存,但是还没有数据。为了得到数据,常见的操作就是Map:
//资源的索引,资源的范围(nullptr表示全部范围),资源
ThrowIfFailed(mUploadBuffer->Map(0, nullptr, reinterpret_cast<void**>(&mMappedData)));
将mMappedData里边的数据映射到mUploadBuffer,那么mMappedData的数据如果更改了,在mUploadBuffer读到的数据就会更改。
再看怎么更改mMappedData的数据:
memcpy(&mMappedData[elementIndex*mElementByteSize], &data, sizeof(T));
内存拷贝的方式放入mMappedData,使用的时候对应了我们定义的常量数据结构ObjectConstants。
最后:
//资源的索引,资源的范围(nullptr表示全部范围)
mUploadBuffer->Unmap(0, nullptr);
龙书的源码将这个部分封装成了一个模板类,在构造和析构里边分别进行Map和Unmap,并提供一个CopyData进行memcpy。
我们现在创建了常量数据ObjectConstants的上传堆,并且把数据通过映射反应到了上传堆上提供给GPU来读,但是很遗憾,GPU还不知道怎么读,因为我们似乎还一点没说常量缓冲的事情,现在仅仅是数据准备好了而已。
2.常量缓冲区
一般叫做CBV。
1.创建描述符堆:结构体+创建
D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc;
cbvHeapDesc.NumDescriptors = numDescriptors;//,描述符CBV的个数
cbvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
cbvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
cbvHeapDesc.NodeMask = 0;
ThrowIfFailed(md3dDevice->CreateDescriptorHeap(&cbvHeapDesc,//结构体
IID_PPV_ARGS(&mCbvHeap)));//堆的指针
首先是Type定义这是一个CBV或者其他,然后是指定他在Shader中可见,因为我们是要把数据放入流水线。
有了存储描述符的堆,现在就要创建描述符CBV:
//获得描符堆在CPU上的地址,并且根据我们常量存储的相对顺序进行偏移到准确位置
auto handle = CD3DX12_CPU_DESCRIPTOR_HANDLE(mCbvHeap>GetCPUDescriptorHandleForHeapStart());
handle.Offset(heapIndex, mCbvSrvUavDescriptorSize);//最开始初始化计算到的CBV占据空间的大小
//获得常量数据objectCB上传堆的实际存储地址
cbAddress = objectCB->GetGPUVirtualAddress();
// Offset to the ith object constant buffer in the buffer.
cbAddress += i*objCBByteSize;
//填写描述符CBV结构体,包含在堆中的存储位置,以及需要的存储空间
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;
cbvDesc.BufferLocation = cbAddress;//CBV存储了上传堆存数据的位置
cbvDesc.SizeInBytes = objCBByteSize;
//将CBV存入描述符堆
md3dDevice->CreateConstantBufferView(&cbvDesc, handle);
以上,mCbvHeap的cpu地址和objectCB(uploadBuffer)的GPU地址发生关联,也就是mCbvHeap和uploadBuffer关联了。
3.数据流转渠道
我们现在做了三件事:
1.准备好了数据,并建立了上传堆对数据的映射;
2.建立了CBV并存进了他的堆;
3.在CBV的结构体描述中存储了上传堆的数据地址。
CPU负责将数据Copy到指定的dataMap,然后映射到GPU(资源的创建和更新)
CBV堆上的CBV记录了GPU资源的地址,以便对这部分资源进行解释;
为了让GPU看懂CBV这份说明书,答案就是根签名。
二、根签名
这是一个三级结构:
根签名——>根参数——>根描述符表/根常量/根描述符
1.创建跟签名
实际设置数据的定义是在最底层(第三层)来做的。
将它看作一个数组来理解,在根描述符表中:
CD3DX12_DESCRIPTOR_RANGE cbvTable0;
cbvTable0.Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0);//解释他是CBV的,描述符的数量,寄存器槽。
第一个参数指定了类型CBV的(b),第三个参数指定了这是0号,两者组合出了(b0)
这段代码首先定义一个无类型的根描述符表,然后使用Init函数进行初始化。
2.根参数
第二级根参数:
CD3DX12_ROOT_PARAMETER slotRootParameter[1];
slotRootParameter[0].InitAsDescriptorTable(1, &cbvTable0);//描述符表个数,描述符表也是定义+赋值
3.根签名
第三级根签名:
//根参数数组的size,根参数,0,nullptr,指定根签名允许输入装配,输入布局等的权限
CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(1, slotRootParameter, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
以上只是填写了根签名的结构体,还没有创建,创建过程如下:
首先使用序列化,这是DX12要求的,然后调用创建根签名CreateRootSignature:
ComPtr<ID3DBlob> serializedRootSig = nullptr;
ComPtr<ID3DBlob> errorBlob = nullptr;
HRESULT hr = D3D12SerializeRootSignature(&rootSigDesc, D3D_ROOT_SIGNATURE_VERSION_1,
serializedRootSig.GetAddressOf(), errorBlob.GetAddressOf());
ThrowIfFailed(md3dDevice->CreateRootSignature(
0,
serializedRootSig->GetBufferPointer(),//序列化的地址
serializedRootSig->GetBufferSize(),//序列化的空间大小
IID_PPV_ARGS(mRootSignature.GetAddressOf())));
三个级别都弄好了,描述符表用来定义根参数,根参数用来定义根签名,这相当于这个三级目录我们搭建好了,剩下的工作就是如何使用。
2.使用跟签名
1.激活当前使用的根签名(放入命令列表的,也就意味着放入流水线)
表示让哪个根签名处于活跃状态,因为后续马上会使用
mCommandList->SetGraphicsRootSignature(mRootSignature.Get());
2.激活CBV堆
CBV具有数据地址,激活CBV堆意味着激活了一些数据
(内部蕴含的逻辑:传到堆的CPU地址的数据会内部自动处理到GPU地址,否则此处拿GPU数据为null)
ID3D12DescriptorHeap* descriptorHeaps[] = { mCbvHeap.Get() };
mCommandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps);
3.对两者进行关联
将激活的数据(在GPU)与激活的根签名记录的槽对齐
//先得到CBV在GPU里边的地址
auto cbvHandle = CD3DX12_GPU_DESCRIPTOR_HANDLE(mCbvHeap->GetGPUDescriptorHandleForHeapStart());
cbvHandle.Offset(cbvIndex, mCbvSrvUavDescriptorSize);//偏移到准确地址
//第一个参数根参数数据索引(寄存器槽和类型),第二个参数CBV地址=>数据对齐
//第0号根参数,根据定义描述了b0,那么就把cbvHandle数据(cbvHeap->uploadBuffer)装载到b0
cmdList->SetGraphicsRootDescriptorTable(0, cbvHandle);
1.在CPU端定义/更新数据,CopyData到上传堆;上传堆地址是常量描述符的数据内容,创建CBV时关联;
2.利用根签名定义数据类型和取数据的槽(b0);
3.同时激活根签名和描述符堆(描述符解释数据(上传堆地址,格式等)),将数据对齐到槽,定义了数据流通的渠道;
4.shader解析的是时候,自动把关联的数据填充到标记有register(b0)的结构中赋值,进行使用。