-
Notifications
You must be signed in to change notification settings - Fork 409
PSOs, Shaders, and Signatures
DirectXTK |
---|
In order to render using DirectX 12, the complete description of the render state needs to be captured in a ID3D12PipelineState
interface object (PSO). Compiled shaders for all stages you use are bound to the PSO at creation time along with the vertex buffer input layout. In order to share data between the HLSL shader and the CPU, the ID3D12RootSignature
interface object describes how the shader expects parameters to be bound and is also part of the PSO.
Root signature management is one of the more challenging aspects of using DirectX 12. In Direct3D 11, you can think of every shader using the same 'mega signature', but in DirectX 12 you rarely need anything like the 128+ slots per stage that DirectX 11 provides. In DirectX Tool Kit for DirectX 12 we have a dozen or so different root signatures used internally used in different aspects.
Here's a basic root signature we use for BasicEffect
when texturing using the D3DX12 utility library and DirectXHelpers function CreateRootSignature
which combines the underlying D3D12SerializeRootSignature
and ID3D12Device::CreateRootSignature
calls:
// Create root signature.
enum RootParameterIndex
{
ConstantBuffer,
TextureSRV,
TextureSampler,
RootParameterCount
};
Microsoft::WRL::ComPtr<ID3D12RootSignature> rootSig;
{
D3D12_ROOT_SIGNATURE_FLAGS rootSignatureFlags =
D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT |
D3D12_ROOT_SIGNATURE_FLAG_DENY_DOMAIN_SHADER_ROOT_ACCESS |
D3D12_ROOT_SIGNATURE_FLAG_DENY_GEOMETRY_SHADER_ROOT_ACCESS |
D3D12_ROOT_SIGNATURE_FLAG_DENY_HULL_SHADER_ROOT_ACCESS;
// Create root parameters and initialize first (constants)
CD3DX12_ROOT_PARAMETER rootParameters[RootParameterIndex::RootParameterCount] = {};
rootParameters[RootParameterIndex::ConstantBuffer].InitAsConstantBufferView(0, 0, D3D12_SHADER_VISIBILITY_ALL);
// Root parameter descriptor
CD3DX12_ROOT_SIGNATURE_DESC rsigDesc = {};
// Include texture and srv
CD3DX12_DESCRIPTOR_RANGE textureSRV(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0);
CD3DX12_DESCRIPTOR_RANGE textureSampler(D3D12_DESCRIPTOR_RANGE_TYPE_SAMPLER, 1, 0);
rootParameters[RootParameterIndex::TextureSRV].InitAsDescriptorTable(1, &textureSRV, D3D12_SHADER_VISIBILITY_PIXEL);
rootParameters[RootParameterIndex::TextureSampler].InitAsDescriptorTable(1, &textureSampler, D3D12_SHADER_VISIBILITY_PIXEL);
// use all parameters
rsigDesc.Init(static_cast<UINT>(std::size(rootParameters)), rootParameters, 0, nullptr, rootSignatureFlags);
DX::ThrowIfFailed(CreateRootSignature(device, &rsigDesc, rootSig.GetAddressOf()));
}
In addition to programmatically building root signatures, they can also be declared in HLSL. The root signature above is described as:
#define RootSig \
"RootFlags ( ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT |" \
" DENY_DOMAIN_SHADER_ROOT_ACCESS |" \
" DENY_GEOMETRY_SHADER_ROOT_ACCESS |" \
" DENY_HULL_SHADER_ROOT_ACCESS )," \
"CBV(b0),"\
"DescriptorTable ( SRV(t0), visibility = SHADER_VISIBILITY_PIXEL ),"\
"DescriptorTable ( Sampler(s0), visibility = SHADER_VISIBILITY_PIXEL )"
[RootSignature(RootSig)]
VSOutput VertexShader(VSInput vin)
{
...
}
On the Xbox platform, it's best practice to do HLSL root signatures to avoid triggering a second-phase compilation at runtime. On the PC/UWP platform, if you provide a programmatic root signature then any HLSL declared root signature is ignored.
For DirectX Tool Kit, we've adopted doing both programmatic root signatures and have matching HLSL rootsigs for built-in Effects as developer education for how to write both kinds.
Shaders are typically built offline at build time and loaded as binary blobs, or they can be compiled 'on-the-fly' from HLSL source files. The process of building these shaders is the same as it was in DirectX 11--see Writing custom shaders for a quick tutorial.
Using the ReadData.h helper, we can load these blobs at runtime:
auto vertexShaderBlob = DX::ReadData(L"VertexShader.cso");
D3D12_SHADER_BYTECODE vertexShader = { vertexShaderBlob.data(), vertexShaderBlob.size() };
auto pixelShaderBlob = DX::ReadData(L"PixelShader.cso");
D3D12_SHADER_BYTECODE pixelShader = { pixelShaderBlob.data(), pixelShaderBlob.size() };
The built-in shaders used by DirectX Tool Kit are included as headers containing const C++ arrays to avoid the need to ship binary blobs as individual files.
The detail on how data in a vertex buffer is organized is described in the input layout.
static const D3D12_INPUT_ELEMENT_DESC s_inputElementDesc[] =
{
{ "SV_Position", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT,
D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT,
D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
};
D3D12_INPUT_LAYOUT_DESC inputLayout = { s_inputElementDesc, static_cast<UINT>(std::size(s_inputElementDesc)) };
The VertexTypes header contains many common vertex formats with their associated
D3D12_INPUT_LAYOUT_DESC
Once you have a root signature, the required shaders, and the input layout, you are ready to create a Pipline State Object (PSO). This can be done manually with DirectX 12 APIs, but here we use the EffectPipelineStateDescription helper class along with the RenderTargetState and CommonStates classes.
RenderTargetState rtState(m_deviceResources->GetBackBufferFormat(),
m_deviceResources->GetDepthBufferFormat());
EffectPipelineStateDescription pd(
&inputLayout,
CommonStates::Opaque,
CommonStates::DepthDefault,
CommonStates::CullCounterClockwise,
rtState);
Microsoft::WRL::ComPtr<ID3D12PipelineState> pso;
pd.CreatePipelineState(device, rootSignature.Get(), vertexShader, pixelShader,
pso.GetAddressOf());
Note that the state captured in a PSO includes details about the render target configuration and format you will be using to render in addition to standard rasterizer state, blend state, and depth/stencil state. The specific input layout of the vertex buffer(s) is also required at creation time. Because all of this is 'baked' into PSOs, you may have to create a number of them to cover all the different state combinations used throughout your scene rendering.
Now for the payoff. Since almost all the state information was captured in the PSO, rendering with it is quite simple:
// Need to bind descriptor heaps for the texture and samplers
ID3D12DescriptorHeap* heaps[] = { m_resourceDescriptors->Heap(), m_states->Heap() };
commandList->SetDescriptorHeaps(static_cast<UINT>(std::size(heaps)), heaps);
// Bind the root signature we are using
commandList->SetGraphicsRootSignature(rootSignature.Get());
// Set the parameters into our root signature
commandList->SetGraphicsRootDescriptorTable(RootParameterIndex::TextureSRV, /* texture index */);
commandList->SetGraphicsRootDescriptorTable(RootParameterIndex::TextureSampler, /* sampler index */);
commandList->SetGraphicsRootConstantBufferView(RootParameterIndex::ConstantBuffer, /* CB GPU address */);
// Set the PSO we are using
commandList->SetPipelineState(pso.Get());
// Draw here...
In DirectX Tool Kit for DirectX 12, the individual Effects, PostProcess, and SpriteBatch objects include per-device root signature and a per-instance pipeline state object. These are bound to a command-list when the Apply method is invoked.
A DirectX 12 Pipeline State Object is immutable and contains all the state that DirectX 11 managed through the input layout object, blend state object, rasterizer state object, depth/stencil state object, and sampler state objects. The only dynamic data you can provide at render time are some of the control values:
Value | DirectX 11 | DirectX12 |
---|---|---|
FLOAT BlendFactor[4] | OMSetBlendState | OMSetBlendFactor |
UINT StencilRef | OMSetDepthStencilState | OMSetStencilRef |
You can set the primitive topology with IASetPrimitiveTopology
, but you are limited to the values that are compatible the active PSO's D3D12_GRAPHICS_PIPELINE_STATE_DESC.PrimitiveTopologyType
setting.
You also have dynamic control over the viewport setting via RSSetViewports
but you need to explicitly set RSSetScissorRects
as well.
Note that when you
Close
and a command-list and thenReset
it later for re-use, all state is reset to defaults. As such, you need to set all required state every render frame.
SimpleTriangle12 sample for PC, UWP, Microsoft GDK for Xbox, and Xbox One XDK
All content and source code for this package are subject to the terms of the MIT License.
This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact [email protected] with any additional questions or comments.
- Universal Windows Platform apps
- Windows desktop apps
- Windows 11
- Windows 10
- Xbox One
- Xbox Series X|S
- x86
- x64
- ARM64
- Visual Studio 2022
- Visual Studio 2019 (16.11)
- clang/LLVM v12 - v18
- MinGW 12.2, 13.2
- CMake 3.20