到目前为止展示过编写输入布局描述、创建顶点着色器和像素着色器,以及配置光栅器状态组这 3 个步骤。接下来讲如何将这些对象绑定到图形流水线上,用以实际绘制图形。大多数控制图形流水线状态的对象被统称为流水线状态对象 (Pipeline State Object,PSO),用 ID3D12PipelineState 接口来表示。要创建 PSO,我们首先要填写一份描述其细节的 D3D12_GRAPHICS_PIPELINE_STATE_DESC 结构体实例。
cpp
typedef struct D3D12_GRAPHICS_PIPELINE_STATE_DESC
{
ID3D12RootSignature *pRootSignature;
D3D12_SHADER_BYTECODE VS;
D3D12_SHADER_BYTECODE PS;
D3D12_SHADER_BYTECODE DS;
D3D12_SHADER_BYTECODE HS;
D3D12_SHADER_BYTECODE GS;
D3D12_STREAM_OUTPUT_DESC StreamOutput;
D3D12_BLEND_DESC BlendState;
UINT SampleMask;
D3D12_RASTERIZER_DESC RasterizerState;
D3D12_DEPTH_STENCIL_DESC DepthStencilState;
D3D12_INPUT_LAYOUT_DESC InputLayout;
D3D12_PRIMITIVE_TOPOLOGY_TYPE PrimitiveTopologyType;
UINT NumRenderTargets;
DXGI_FORMAT RTVFormats[8];
DXGI_FORMAT DSVFormat;
DXGI_SAMPLE_DESC SampleDesc;
} D3D12_GRAPHICS_PIPELINE_STATE_DESC;
-
pRootSignature:指向一个与此 PSO 相绑定的根签名的指针。该根签名一定要与此 PSO 指定的着色器相兼容。
-
VS:待绑定的顶点着色器。此成员由结构体 D3D12_SHADER_BYTECODE 表示,这个结构体存有指向已编译好的字节码数据的指针,以及该字节码数据所占的字节大小。
cpp
typedef struct D3D12_SHADER_BYTECODE {
const void *pShaderBytecode;
SIZE_T BytecodeLength;
} D3D12_SHADER_BYTECODE;
-
PS:待绑定的像素着色器。
-
DS:待绑定的域着色器(我们将在后续章节中讲解此类型的着色器)。
-
HS:待绑定的外壳着色器(我们将在后续章节中讲解此类型的着色器)。
-
GS:待绑定的几何着色器(我们将在后续章节中讲解此类型的着色器)。
-
StreamOutput:用于实现一种称作流输出(stream-out)的高级技术。目前我们仅将此字段清零。
-
BlendState:指定混合(blending)操作所用的混合状态。我们将在后续章节中讨论此状态组,目前仅将此成员指定为默认的 CD3DX12_BLEND_DESC(D3D12_DEFAULT)。
-
SampleMask:多重采样最多可采集 32 个样本。借此参数的 32 位整数值,即可设置每个采样点的采集情况(采集或禁止采集)。例如,若禁用了第 5 位(将第 5 位设置为 0),则将不会对第 5 个样本进行采样。当然,要禁止采集第 5 个样本的前提是,所用的多重采样至少要有 5 个样本。假如一个应用程序仅使用了单采样(single sampling),那么只能针对该参数的第 1 位进行配置。一般来说,使用的都是默认值 0xffffffff,即表示对所有的采样点都进行采样。
-
RasterizerState:指定用来配置光栅器的光栅化状态。
-
DepthStencilState:指定用于配置深度/模板测试的深度/模板状态。我们将在后续章节中对此状态进行讨论,目前只把它设为默认的 CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT)。
-
InputLayout:输入布局描述,此结构体中有两个成员:一个由 D3D12_INPUT_ELEMENT_DESC 元素构成的数组,以及一个表示此数组中元素数量的无符号整数。
cpp
typedef struct D3D12_INPUT_LAYOUT_DESC
{
const D3D12_INPUT_ELEMENT_DESC *pInputElementDescs;
UINT NumElements;
} D3D12_INPUT_LAYOUT_DESC;
- PrimitiveTopologyType:指定图元的拓扑类型。
cpp
typedef enum D3D12_PRIMITIVE_TOPOLOGY_TYPE {
D3D12_PRIMITIVE_TOPOLOGY_TYPE_UNDEFINED = 0,
D3D12_PRIMITIVE_TOPOLOGY_TYPE_POINT = 1,
D3D12_PRIMITIVE_TOPOLOGY_TYPE_LINE = 2,
D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE = 3,
D3D12_PRIMITIVE_TOPOLOGY_TYPE_PATCH = 4
} D3D12_PRIMITIVE_TOPOLOGY_TYPE
-
NumRenderTargets:同时所用的渲染目标数量(即 RTVFormats 数组中渲染目标格式的数量)。
-
RTVFormats:渲染目标的格式。利用该数组实现向多渲染目标同时进行写操作。使用此 PSO 的渲染目标的格式设定应当与此参数相匹配。
-
DSVFormat:深度/模板缓冲区的格式。使用此 PSO 的深度/模板缓冲区的格式设定应当与此参数相匹配。
-
SampleDesc:描述多重采样对每个像素采样的数量及其质量级别。此参数应与渲染目标的对应设置相匹配。
在 D3D12_GRAPHICS_PIPELINE_STATE_DESC 实例填写完毕后,我们即可用 ID3D12Device::CreateGraphicsPipelineState方法来创建 ID3D12PipelineState 对象。
cpp
// BoxApp.cpp 58行
ComPtr mRootSignature;
std::vector mInputLayout;
ComPtr mvsByteCode;
ComPtr mpsByteCode;
...
// BoxApp.cpp 436行 BuildPSO()
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc;
ZeroMemory(&psoDesc, sizeof(D3D12_GRAPHICS_PIPELINE_STATE_DESC));
psoDesc.InputLayout = { mInputLayout.data(), (UINT)mInputLayout.size() };
psoDesc.pRootSignature = mRootSignature.Get();
psoDesc.VS =
{
reinterpret_cast(mvsByteCode->GetBufferPointer()),
mvsByteCode->GetBufferSize()
};
psoDesc.PS =
{
reinterpret_cast(mpsByteCode->GetBufferPointer()),
mpsByteCode->GetBufferSize()
};
psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
psoDesc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
psoDesc.SampleMask = UINT_MAX;
psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
psoDesc.NumRenderTargets = 1;
psoDesc.RTVFormats[0] = mBackBufferFormat;
psoDesc.SampleDesc.Count = m4xMsaaState ? 4 : 1;
psoDesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;
psoDesc.DSVFormat = mDepthStencilFormat;
ComPtr mPSO;
md3dDevice->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&mPSO)));
ID3D12PipelineState 对象集合了大量的流水线状态信息。为了保证性能,我们将所有这些对象都集总在一起,一并送至渲染流水线。通过这样的一个集合,Direct3D 便可以确定所有的状态是否彼此兼容,而驱动程序则能够据此而提前生成硬件本地指令及其状态。
注意:由于 PSO 的验证和创建操作过于耗时,所以应在初始化期间就生成 PSO。除非有特别的需求,例如,在运行时创建 PSO 伊始就要当即对它进行第一次引用的这种情况。随后,我们就可将它存于如散列表(哈希表)这样的集合里,以便在后续使用时快速获取。
并非所有的渲染状态都封装于 PSO 内,如视口(viewport)和裁剪矩形(scissor rectangle)等属性就独立于 PSO。由于将这些状态的设置与其他的流水线状态分隔开来会更有效,所以把它们强行集中在 PSO 内也并不会为之增添任何优势。
Direct3D 实质上就是一种状态机(state machine),里面的事物会保持它们各自的状态,直到我们将其改变。如果我们以不同的 PSO 去绘制不同物体,则需要像下面那样来组织代码:
cpp
// 重置命令列表并指定初始 PSO
mCommandList->Reset(mDirectCmdListAlloc.Get(), mPSO1.Get());
/* ......使用 PSO 1绘制物体...... */
// 改变 PSO
mCommandList->SetPipelineState(mPSO2.Get());
/* ......使用 PSO 2绘制物体...... */
// 改变 PSO
mCommandList->SetPipelineState(mPSO3.Get());
/* ......使用 PSO 3绘制物体...... */
换句话说,如果把一个 PSO 与命令列表相绑定,那么,在我们设置另一个 PSO 或重置命令列表之前,会一直沿用当前的 PSO 绘制物体。
考虑到程序的性能问题,我们应当尽可能减少改变 PSO 状态的次数。为此,若能以一个 PSO 绘制出所有的物体,绝不用第二个 PSO。切记,不要在每次绘制调用时都修改 PSO。