Skip to content

PSOs, Shaders, and Signatures

Chuck Walbourn edited this page Apr 26, 2022 · 26 revisions
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.

Creating root signatures

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.

Load or compile shaders

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.

Declaring the input layout

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

Create pipeline state objects

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.

Binding

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...

Using Effects

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.

State Management

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 then Reset it later for re-use, all state is reset to defaults. As such, you need to set all required state every render frame.

Further reading

SimpleTriangle12 sample for PC, UWP, Microsoft GDK for Xbox, and Xbox One XDK

Root Signatures

For Use

  • Universal Windows Platform apps
  • Windows desktop apps
  • Windows 11
  • Windows 10
  • Xbox One
  • Xbox Series X|S

For Development

  • Visual Studio 2022
  • Visual Studio 2019 (16.11)
  • clang/LLVM v12 - v18
  • MinGW 12.2, 13.2
  • CMake 3.20

Related Projects

DirectX Tool Kit for DirectX 11

DirectXMesh

DirectXTex

DirectXMath

Tools

Test Suite

Model Viewer

Content Exporter

DxCapsViewer

See also

DirectX Landing Page

Clone this wiki locally