Root signature and pipeline state

When rendering anything with any modern rendering API, you are required to specify state and shaders in order to tell the GPU how the rendering should be done: Which primitives should not contribute to the final image? Should some of the pixels be transparent, and how is transparency determined?

In order to specify state, older APIs (OpenGL & DirectX 1-9) contained function calls to set individual state properties. For instance, in order to have a wireframe mode, OpenGL required a call to glPolygonMode with GL_LINE as a property.

In DirectX 11, Microsoft opted for having objects with combined state based on rough categories in order to reduce the amount of state that had to be managed individually.

In DirectX 12, state and shaders have all been combined into one single object: The ID3D12PipelineState. This object takes in all objects traditionally related to:

  • Root signature
  • Steam output
  • Blend state
  • Rasterizer state
  • Depth stencil state
  • Input layout
  • Primitive type
  • Output render targets
  • Multisampling properties

In addition, shader bytecode has to be provided for the pipeline state, and is hardcoded within the object.

The advantage to doing this is reducing the amount of calls to functions changing the state, and the ability to optimize the shaders and pipeline to the exact properties involved.

For the creation of a pipeline state, a root signature is required. The root signature is an object intended to manage all resource bindings in a DirectX 12 application. The root signature contains a set of slots for setting properties to, and the pipeline state will use the slot layout in order to check whether or not the shaders are compatible and for mapping the slots to the properties in the shader. This will allow all root signatures with identical layouts to be supported by the pipeline. Swapping the root signature with any other compatible root signature will swap all bindings in all slots in a single API call. This can be very convenient if a lot of state needs to be changed between two draw calls, allowing you to bake all resource bindings in a state object and simply swapping the state object rather than setting all bindings individually.

To create the root signature, we first need to serialize the root signature layout. This constructs a layout convenient to the DirectX 12 runtime to use to create new root signatures and to match with the pipeline state.

ID3DBlob* rsBlob;
ID3DBlob* errorBlob;

result = D3D12SerializeRootSignature (
    &(D3D12_ROOT_SIGNATURE_DESC) {
        .NumParameters = 3,
        .pParameters = (D3D12_ROOT_PARAMETER[3]){
            { .ShaderVisibility = D3D12_SHADER_VISIBILITY_VERTEX, .ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS,  .Constants = { .Num32BitValues = 50 } },
            { .ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL,  .ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE, .DescriptorTable = {
                .NumDescriptorRanges = 1, .pDescriptorRanges = (D3D12_DESCRIPTOR_RANGE[1]) { { .RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV, .NumDescriptors = 128 } } } },
            { .ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL,  .ParameterType = D3D12_ROOT_PARAMETER_TYPE_32BIT_CONSTANTS,  .Constants = { .Num32BitValues = 1  } },
        },
        .NumStaticSamplers = 1,
        .pStaticSamplers = (D3D12_STATIC_SAMPLER_DESC[1]) {
            { .ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL, .Filter = D3D12_FILTER_ANISOTROPIC, .AddressU = D3D12_TEXTURE_ADDRESS_MODE_WRAP,
                .AddressV = D3D12_TEXTURE_ADDRESS_MODE_WRAP, .AddressW = D3D12_TEXTURE_ADDRESS_MODE_WRAP, .MaxLOD = 1000.0f, .MaxAnisotropy = 16 }
        },
        .Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT,
    },
    D3D_ROOT_SIGNATURE_VERSION_1,
    &rsBlob,
    &errorBlob
);
const char* err = "";
if ( !SUCCEEDED(result) )
{
    err = errorBlob->lpVtbl->GetBufferPointer ( errorBlob );
    RETURN_ERROR(-1, "D3D12SerializeRootSignature failed (0x%08X) (%s)", result, errorBlob->lpVtbl->GetBufferPointer ( errorBlob ) );
}

result = renderer->device->lpVtbl->CreateRootSignature (
    renderer->device,
    0,
    rsBlob->lpVtbl->GetBufferPointer ( rsBlob ),
    rsBlob->lpVtbl->GetBufferSize ( rsBlob ),
    &IID_ID3D12RootSignature,
    &renderer->rs
);
if ( !SUCCEEDED(result) )
    RETURN_ERROR(-1, "CreateRootSignature failed (0x%08X)", result );
rsBlob->lpVtbl->Release ( rsBlob );

With the root signature serialized and created, we can now move onto creating the pipeline state object we are going to be using for this example. In realistic scenarios you will have more than a single pipeline state object, but for the current demo we can safely get by with a single pipeline state object.

const asset_t *vsAsset, *psAsset;
renderer->vtbl.GetAsset ( "assets/shader_bin/dx12/basic_vs.cso", &vsAsset );
renderer->vtbl.GetAsset ( "assets/shader_bin/dx12/basic_ps.cso", &psAsset );

result = renderer->device->lpVtbl->CreateGraphicsPipelineState (
    renderer->device,
    &(D3D12_GRAPHICS_PIPELINE_STATE_DESC) {
        .pRootSignature = renderer->rs,
        .VS = { .pShaderBytecode = vsAsset->data, .BytecodeLength = vsAsset->size },
        .PS = { .pShaderBytecode = psAsset->data, .BytecodeLength = psAsset->size },
        .SampleMask = 0xFFFFFFFF,
        .RasterizerState = {
            .FillMode = D3D12_FILL_MODE_SOLID,
            .CullMode = D3D12_CULL_MODE_FRONT,
        },
        .InputLayout = {
            .NumElements = 2,
            .pInputElementDescs = (D3D12_INPUT_ELEMENT_DESC[2]){
                {
                    .SemanticName   = "POSITION",
                    .Format         = DXGI_FORMAT_R32G32B32_FLOAT,
                    .InputSlot      = 0,
                    .InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA
                },
                {
                    .SemanticName   = "TEXCOORD",
                    .Format         = DXGI_FORMAT_R32G32_FLOAT,
                    .InputSlot      = 1,
                    .InputSlotClass = D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA
                },
            },
        },
        .DepthStencilState = {
            .DepthEnable = TRUE,
            .DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ALL,
            .DepthFunc = D3D12_COMPARISON_FUNC_LESS_EQUAL,
        },
        .DSVFormat = DXGI_FORMAT_D32_FLOAT,
        .BlendState.RenderTarget[0].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL,
        .PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE,
        .NumRenderTargets = 1,
        .RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM,
        .SampleDesc.Count = 1,
    },
    &IID_ID3D12PipelineState,
    &renderer->pso
);
if ( !SUCCEEDED(result) )
    RETURN_ERROR(-1, "CreateGraphicsPipelineState failed (0x%08X)", result );

The shaders are compiled from the following source code:

struct ApplicationToVertex
{
    float3 position : POSITION;
    float2 texcoord : TEXCOORD;
};

struct VertexToPixel
{
    float4 position : SV_POSITION;
    float2 texcoord : TEXCOORD;
};

struct PerFrameConstantsVS
{
    float4x4 mvpMat;
};

struct PerFrameConstantsPS
{
    uint textureIndex;
};

////////////////////////////////////////
// VS
ConstantBuffer PerFrameVS : register(b0);

VertexToPixel mainVS ( ApplicationToVertex IN )
{
    VertexToPixel OUT;

    OUT.position = mul ( PerFrameVS.mvpMat, float4 ( IN.position, 1.0f ) );
    OUT.texcoord = IN.texcoord;
    return OUT;
}

////////////////////////////////////////
// PS
ConstantBuffer PerFramePS : register(b0);

Texture2D Tex[128] : register(t0);
SamplerState Sampler : register(s0);

float4 mainPS ( VertexToPixel IN ) : SV_TARGET
{
    return Tex[PerFramePS.textureIndex].Sample ( Sampler, IN.texcoord );
}