⭐描述符&根参数
知识所在龙书章节:
P83~84 资源与描述符
P201~206 常量缓冲区描述符 根签名 描述符表
P262~271 根签名
①资源与描述符
GPU资源并非直接与渲染流水线相绑定,而是通过一种叫描述符的对象来间接引用
描述符(descriptor)/视图(view):一种对送往GPU的资源进行描述的轻量级结构
描述符作为中间层,GPU能了解到资源的必要信息,因为GPU资源本质是一些普通的内存块,需要描述符加以说明
常用描述符:
- CBV:const buffer view 常量缓冲区视图
- SRV:shader resource view 着色器资源视图
- UAV:unordered access view 无序访问视图
- 采样器sampler描述符
- RTV:render target view 渲染目标视图资源
- DSV:depth/stencil view 深度模板视图资源
描述符堆:存放用户程序中某种特定类型描述符的一块内存
我们需要为每一种类型的描述符都创建单独的描述符堆,也可以为同一种描述符类型创建多个描述符堆
多个描述符可以引用同一个资源
创建描述符的最佳时机为初始化期间
②常量缓冲区描述符
常量缓冲区描述符都要存放在以D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV类型所建的描述符堆中,这种堆可以混合存储CBV、SRV、UAV描述符
// 填写堆结构
D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc;
cbvHeapDesc.NumDescriptors = 1; // 堆中描述符个数
cbvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; // 堆的类型
cbvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE; // 堆的选项
cbvHeapDesc.NodeMask = 0; // 对于单适配器操作,请将此值设置为零
// 与RTV和DSV的主要区别:Type设置为..VISIBLE
ThrowIfFailed(md3dDevice->CreateDescriptorHeap(&cbvHeapDesc,
IID_PPV_ARGS(&mCbvHeap)));
D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE:
可以选择在描述符堆上设置标志D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE,以指示它已绑定到命令列表上以供着色器引用。为方便起见,在没有此标志的情况下创建的描述符堆允许应用程序在CPU内存中暂存描述符,然后再将其复制到着色器可见描述符堆。但是,对于应用程序来说,直接将描述符创建到着色器可见的描述符堆中也是可以的,而无需在CPU上暂存任何内容
③根签名和描述符表
不同类型的资源会被绑定到特定的寄存器槽(register slot)上,以供着色器程序访问
纹理资源 t0 t1 ...
采样器资源 s0 s1 ...
常量缓冲区资源 b0 b1 ...
寄存器槽就是向着色器传递资源的手段,register(*#)中的*表示寄存器传递的资源类型,可以是t(着色器资源视图)、s(采样器)、u(无序访问视图)以及b(常量缓冲区视图),#则为所用的寄存器编号
根签名(root signature):在执行绘制命令之前,那些应用程序将绑定到渲染流水线上的资源,它们会被映射到着色器的对应输入寄存器
"根签名"一词的由来:
输入资源作为shader的函数参数,根签名定义了函数签名
根签名由ID3D12RootSignature接口来表示,并以一组描述绘制调用过程中着色器所需资源的**根参数(root parameter)**定义而成
根参数包括:根常量、根描述符、描述符表
描述符表:指定的是描述符堆中存有描述符的一块连续区域
// 代码示例:
// 创建一个根签名,它的根参数为一个描述符表,其大小足以容纳一个CBV
// 1.根参数
CD3DX12_ROOT_PARAMETER slotRootParameter[1];
// 2.填写描述符表的结构
CD3DX12_DESCRIPTOR_RANGE cbvTable;
cbvTable.Init(
D3D12_DESCRIPTOR_RANGE_TYPE_CBV,
1, // 描述符数量
0 // 将这段描述符区域绑定至此基准着色器寄存器(base shader register)
);
// 3.创建描述符表
slotRootParameter[0].InitAsDescriptorTable(
1, // 描述符区域的数量 -- 注意是区域的数量而不是描述符的数量(CBV区域、...区域)
&cbvTable // 指向描述符区域数组的指针
);
// 4.根签名由一组根参数构成
CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(1, slotRootParameter, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
// 5.创建仅含一个槽位(该槽位指向一个仅由单个常量缓冲区组成的描述符区域)的根签名
ComPtr<ID3DBlob> serializeRootSig = 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))
);
Direct3D 12规定,必须先将根签名的描述布局进行序列化处理(serialize),待其转换为以ID3DBlob接口表示的序列化数据格式后,才可将它传入CreateRootSignature方法,正式创建根签名
根签名只定义了应用程序要绑定到渲染流水线的资源,却没有真正地执行任何资源绑定操作
只要率先通过命令队列设置好根签名,我们就能用ID3D12GraphicsCommandList::SetGraphicsRootDescriptorTable方法令描述符表与渲染流水线相绑定
// 代码示例:
// 先将根签名和CBV堆设置到命令队列上,并随后再通过设置描述符表来指定我们希望渲染到渲染流水线的资源
// 1.绑定根签名
mCommandList->SetGraphicsRootSignature(mRootSignature.Get());
// 2.绑定描述符堆
ID3D12DescriptorHeap* descriptorHeaps[] = { mCbvHeap.Get() };
mCommandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps);
// 3.偏移到此次绘制调用所需的CBV处
CD3DX12_GPU_DESCRIPTOR_HANDLE cbv(mCbvHeap->GetGPUDescriptorHandleForHeapStart());
cbv.Offset(cbvIndex, mCbvSrvUavDescriptorSize);
// 4.绑定描述符表
mCommandList->SetGraphicsRootDescriptorTable(0, cbv);
④细探根签名
根签名是由一系列根参数定义而成,之前我们创建过存有一个描述符表的根参数
根参数:
- 描述符表:描述符表引用的是描述符堆中的一块连续范围,用于确定要绑定的资源
- 根描述符:通过直接设置根描述符即可指示要绑定的资源,而无需将它存于描述符堆中 – 但是只有常量缓冲区的CBV,缓冲区的SRV/UAV才可以以根描述符来实现资源绑定 注意,这意味着纹理的SRV并不能作为根描述符来实现资源绑定
- 根常量:借助根常量可直接绑定一系列32位的常量值
考虑到性能因素,可放入一个根签名的数据以64DWORD为限
描述符表:每个描述符表占用1DWORD
根描述符:每个根描述符(64位的GPU虚拟地址)占用2DWORD
根常量:每个常量32位,占用1DWORD
"我们可以创建出任何组合的根签名,只要它不超过64DWORD的上限即可"
"根常量虽然使用方便,但会使空间消耗增加迅速" 因为一个根常量就要占据1DWORD
根参数结构:CD3DX12_ROOT_PARAMETER
// 是D3D12_ROOT_PARAMETER的扩展,增加了一些辅助初始化函数
typedef struct D3D12_ROOT_PARAMETER{
D3D12_ROOT_PARAMETER_TYPE ParameterType; // 指定根参数的类型:描述符表、根常量、CBV根描述符、SRV根描述符、UAV根描述符
union{
D3D12_ROOT_DESCRIPTOR_TABLE DescriptorTable;
D3D12_ROOT_CONSTANTS Constants;
D3D12_ROOT_DESCRIPTOR Descriptor;
};
D3D12_SHADER_VISIBILITY ShaderVisibility; // 此根参数在着色器程序中的可见性 -- 一般采用D3D12_SHADER_VISIBILITY_ALL -- 例子:假设知道某资源只会在像素着色器中使用,则设置为D3D12_SHADER_VISIBILITY_PIXEL,这样可以使程序的性能得到优化
}D3D12_ROOT_PARAMETER;
1.描述符表:
// 根参数结构中union
// D3D12_ROOT_DESCRIPTOR_TABLE
typedef struct D3D12_ROOT_DESCRIPTOR_TABLE{
UINT NumDescriptorRanges; // 数组元素个数
const D3D12_ROOT_DESCRIPTOR_RANGE *pDescriptorRanges; // 数组
} D3D12_ROOT_DESCRIPTOR_TABLE;
// 其中 D3D12_ROOT_DESCRIPTOR_RANGE:
typedef struct D3D12_ROOT_DESCRIPTOR_RANGE{
D3D12_DESCRIPTOR_RANGE_TYPE RangeType; // 此范围中描述符类型
UINT NumDescriptors; // 描述符数量
UINT BaseShaderRegister; // 要绑定到的基准着色器寄存器
UINT RegisterSpace; // 设置寄存器空间(默认为space0) -- 对于资源数组来说,使用多重寄存器会更加方便
UINT OffsetInDescriptorsFromTableStart; // 起始地址偏移量
} D3D12_DESCRIPTOR_RANGE;
// 参数解释:
BaseShaderRegister:
假设range中有3个描述符,类型为CBV,把此参数设置为1,那么这些资源与HLSL寄存器的绑定情况如下: register b1,b2,b3分别与3个描述符绑定
RegisterSpace:
register(t0, space0) register(t0, space1) 看似是同一个寄存器t0,但是存在于不同的空间中
// 各种类型的描述符混合放置在一个描述符表中:
// 2个CBV 3个SRV 1个UAV
CD3DX12_DESCRIPTOR_RANGE descRange[3];
descRange[0].Init(
D3D12_DESCRIPTOR_RANGE_TYPE_CBV,
2,
0,
0,
0
);
descRange[1].Init(
D3D12_DESCRIPTOR_RANGE_TYPE_SRV,
3,
0,
0,
2
);
descRange[2].Init(
D3D12_DESCRIPTOR_RANGE_TYPE_UAV,
1,
0,
0,
5
);
slotRootParameter[0].InitAsDescriptorTable(
3, descRange, D3D12_SHADER_VISIBILITY_ALL
);
// 其中CD3DX12_DESCRIPTOR_RANGE继承于D3D12_DESCRIPTOR_RANGE,并添加了额外的初始函数Init()
// 偏移量可以不填,因为该参数默认为D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND,令Direct3D来根据表中前一个描述符计算偏移量
2.根描述符:
根描述符使用较多,可以使用辅助方法InitAsConstantBufferView来创建根CBV – P277~278
// 根参数结构中union
typedef struct D3D12_ROOT_DESCRIPTOR
{
UINT ShaderRegister; // (数字)要绑定的着色器寄存器
UINT RegisterSpace; // 寄存器空间
} D3D12_ROOT_DESCRIPTOR;
与描述符表需要在描述符堆中设置对应的句柄不同,要配置根描述符,只需简单而又直接地绑定资源的虚拟地址即可
UINT objCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
D3D12_GPU_VIRTUAL_ADDRESS objCBAddress = objectCB->GetGPUVirtualAddress();
// 偏移到缓冲区中此物体常量的地址
objCBAddress += ri->ObjCBIndex*objCBByteSize;
cmdList->SetGraphicsRootConstantBufferView(
0, // 根参数索引,即将当前根描述符绑定到此编号的寄存器槽位
objCBAddress
);
3.根常量:
// 根参数结构中union
typedef struct D3D12_ROOT_CONSTANTS{
UINT ShaderRegister;
UINT RegisterSpace;
UINT Num32BitValues;
} D3D12_ROOT_CONSTANTS;
设置根常量仍要将数据映射到着色器视角中的常量缓冲区 – 不需要描述符堆
// 根常量使用示例:
// 根签名的定义
CD3DX12_ROOT_PARAMETER slotRootParameter[1];
slotRootParameter[0].InitAsConstants(12, 0); // 12个根常量
// 根签名是一系列根参数
CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(
1, slotRootParameter, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT
);
// 应用程序代码部分:
auto weights = CalcGaussWeights(2.5f);
int blurRadius = (int)weights.size() / 2;
cmdList->SetGraphicsRoot32BitConstants(
0, 1, &blurRadius, 0
);
cmdList->SetGraphicsRoot32BitConstants(
0, (UINT)weights.size(), weights.data(), 1
);
// HLSL代码部分:
// 我们无法获取常量缓冲区中映射有根常量数据的数组元素,所以只能将每个元素分别单独列出
cbuffer cbSettings : register(b0){
int gBlurRadius;
float w0;
float w1;
// ... 11个w值 -- 对应weights数组
float w10;
}
/*
SetGraphicsRoot32BitConstants参数:1.根参数的索引,对应寄存器的槽号2.常量数据个数3.数据指针4.偏移量
*/
总体思路:
1.
纹理:SRV 创建RANGE对象使用Init函数 根参数使用InitAsDescriptorTable实现
常量缓冲区:CBV 根描述符 根参数使用InitAsConstantBufferView实现
根参数数组中,根据变更频率从高到低的顺序进行排列
2.根签名是一系列根参数
CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(3, slotRootParameter, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
// 参数1为根参数个数
3.创建根签名 -- 参考代码
// 创建仅含一个槽位(该槽位指向一个仅由单个常量缓冲区组成的描述符区域)的根签名
ComPtr<ID3DBlob> serializeRootSig = 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))
);
⑤根参数的版本控制
根实参(root argument):我们向根参数传递的实际数值
// 代码示例:
void NormalMapApp::DrawRenderItems(ID3D12GraphicsCommandList* cmdList, const std::vector<RenderItem*>& ritems)
{
UINT objCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
auto objectCB = mCurrFrameResource->ObjectCB->Resource();
// For each render item...
for(size_t i = 0; i < ritems.size(); ++i)
{
auto ri = ritems[i];
cmdList->IASetVertexBuffers(0, 1, &ri->Geo->VertexBufferView());
cmdList->IASetIndexBuffer(&ri->Geo->IndexBufferView());
cmdList->IASetPrimitiveTopology(ri->PrimitiveType);
// 偏移到此帧中渲染项的CBV
D3D12_GPU_VIRTUAL_ADDRESS objCBAddress = objectCB->GetGPUVirtualAddress() + ri->ObjCBIndex*objCBByteSize;
// 指定此次绘制调用所需的描述符
cmdList->SetGraphicsRootConstantBufferView(0, objCBAddress);
// 绘制实例
cmdList->DrawIndexedInstanced(ri->IndexCount, 1, ri->StartIndexLocation, ri->BaseVertexLocation, 0);
}
}
在每次执行绘制调用时,将使用针对当前绘制调用所设置的根实参状态 – 硬件会自动为每次绘制调用保存根实参当时状态的快照(snapshot) – 系统会为每次绘制调用而自动为根参数进行版本控制
SDK文档建议:根签名中的根参数应当按照变更频率,由高到低排列
Direct3D 12文档建议:尽可能避免频繁切换根签名 – 因此好的方法就是令您创建的多个PSO共享同一个根签名
⭐动态顶点缓冲区
之前我们始终将顶点数据存于默认的缓冲区资源中,可借此存储静态几何体,只能一次性设置好数据
此时我们引入动态顶点缓冲区,允许用户频繁地改变其中的顶点数据(每一帧)
之前通过上传缓冲区来更新常量缓冲区中的数据,我们现在可以故技重施,使用上传缓冲区来更新顶点数组
std::unique_ptr<UploadBuffer<Vertex>> WavesVB = nullptr;
WavesVB = std::make_unique<UploadBuffer<Vertex>>(device, waveVertCount, false);
// 创建UpdateWaves函数来每一帧更新缓冲区
void LandAndWavesApp::UpdateWaves(const GameTimer& gt)
{
// Every quarter second, generate a random wave.
static float t_base = 0.0f;
if((mTimer.TotalTime() - t_base) >= 0.25f)
{
t_base += 0.25f;
int i = MathHelper::Rand(4, mWaves->RowCount() - 5);
int j = MathHelper::Rand(4, mWaves->ColumnCount() - 5);
float r = MathHelper::RandF(0.2f, 0.5f);
mWaves->Disturb(i, j, r);
}
// Update the wave simulation.
mWaves->Update(gt.DeltaTime());
// Update the wave vertex buffer with the new solution.
// 上传缓冲区
auto currWavesVB = mCurrFrameResource->WavesVB.get();
for(int i = 0; i < mWaves->VertexCount(); ++i)
{
Vertex v;
v.Pos = mWaves->Position(i);
v.Color = XMFLOAT4(DirectX::Colors::Blue);
// 上传缓冲区
currWavesVB->CopyData(i, v);
}
// Set the dynamic VB of the wave renderitem to the current frame VB.
mWavesRitem->Geo->VertexBufferGPU = currWavesVB->Resource();
}
⭐上传缓冲区
知识所在龙书章节:
P198~201
①常量缓冲区:
因为常量缓冲区需要在每帧进行修改,我们会**把常量缓冲区创建到一个上传堆**而非默认堆中,这样做能使我们从CPU端更新常量
// 假设我们需要绘制n个物体,就需要n个该类型的常量缓冲区
struct ObjectConstants{
DirectX::XMFLOAT4X4 WorldViewProj = MathHelper::Identity4x4();
};
// 常量缓冲区大小必须是256B的倍数
UINT mElementByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
// mUploadCBuffer中存储了一个ObjectConstants类型的常量缓冲区数组
// 绘制物体时,只要将常量缓冲区视图(cbv)绑定到存有物体相应常量数据的缓冲区子区域即可
/// mUploadCBuffer存储的是一个常量缓冲区数组,所以我们称它为c
ComPtr<ID3D12Resource> mUploadCBuffer;
device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(mElementByteSize * NumElements),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&mUploadCBuffer)
);