The Pipeline State Object (PSO) is used to describe how the graphics/compute pipeline will behave in every Pipeline Stage when we are going to render/dispatch something.
We can define behavior of both programmable and fixed pipeline stages, by linking compiler shader code and setting a multitude of variable values along the way. Other settings will be available to change behavior of in-between stages, such as viewport and scissor rect, from a command list instead. The PSO definition will also incorporate the definition of a Root Signature, which is used to define the resources types (and their details) available during execution of the pipeline (e.g. textures exposed to be sampled in pixel shader code).
The PSO is an object that represents the settings for our graphics device (GPU) in order to draw or dispatch something.
Brief description of its components purposes follows:
Shader bytecode: shader code is first compiled and then translated in binary blob data that will serve as an input for the PSO, a process described in the next section.
struct VertexPosColor
{
float3 Position : POSITION;
float3 Color : COLOR;
};
//Used as
VertexShaderOutput main(VertexPosColor IN)
{ … }
Each element is in the form of:
<type> <name> : <Value Semantic>
For the C++ counterpart, we have:
// Create the vertex input layout
D3D12_INPUT_ELEMENT_DESC inputLayout[] = {
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, D3D12_APPEND_ALIGNED_ELEMENT, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
};
Which is a vector of D3D12_INPUT_ELEMENT_DESC defining in detail each element of the per-vertex data that will arrive as input to the vertex shader through input assembler.
Value Semantics are specific keywords used to convey data between pipeline stages (e.g. defining output from vertex shader that will be read as input in hull shader when tessellation is active). They will be especially important for fixed pipeline stages to recognise the data that has to be handled (e.g. POSITION in vertex shader output will be essential for the rasterizer stage to properly handle geometry).
Semantics are required on all variables passed between shader stages.
Names can be completely arbitrary so the programmer can set its own flavours.
System Value Semantics There are certain names however, that are already defined with relative data type and can be used in shaders, two examples are POSITION and COLOR (float4).
The series of defined names will be different between pipeline stages, because the input type is also different (e.g. per-vertex data in vertex shader vs. per-pixel data of pixel shader). Full list of already defined semantics is available in the official documentation.
(SV_) available from DX10, are reserved semantics that have been consequently added. Some of them are new and some of them represent older ones. An example is SV_Position which is used by the rasterizer stage. These variables can be input or output of a shader, depending on the shader type and the purpose of the semantic. For example, SV_PrimitiveID indicates the ID of the primitive that is currently being worked on the pipeline, and this value will be invalid in vertex shader, because a vertex can correspond to multiple primitives (e.g. triangles or patches if tessellation is enabled).
There is a defined mapping for certain system semantics from DX9 to DX10 and beyond, e.g. COLOR is treated as SV_Target and POSITION is treated as SV_Position.
For more information, visit value semantics official documentation.
Primitive topology type (point, line, triangle, or patch) specifies how the input assembler needs to interpret vertex data in order to generate 3D geometry along the pipeline. In D3D12 we can generate either points, lines, triangles or patches (closed geometry that includes more than 3 vertices).
In the PSO this is set using D3D12_PRIMITIVE_TOPOLOGY_TYPE enumeration but it has to be noted that, for each one of those types, there are a set of modes stating how to generate such geometry. This specification is made outside the PSO and described in “In-Command List Settings” chapter below.
Where:
(SC) and (DC) are Source and Destination Color (the first from pixel shader result and the second from the already existing value in render target)
(X) is vector cross product operation
(SBF) and (DBF) Source and Destination Blend Factors, two D3D12_BLEND enum values that will alter contribution of source and destination values for the final result.
(+) is a logic operation among the possible from D3D12_LOGIC_OP entries
(for more info visit Braynzar Soft)
Lastly, we also have the possibility to just blend specific channels among R, G, B and A selected with RenderTargetWriteMask that takes an entry from D3D12_COLOR_WRITE_ENABLE enumeration.
For more information, refer to output merger official documentation
Bias = (float)DepthBias * r + SlopeScaledDepthBias * MaxDepthSlope;
Number of render targets and render target formats
Depth-stencil format data format for the bound depth-stencil buffer
Multisample description describes multisample details for the currently bound render targets with DXGI_SAMPLE_DESC (and render targets must also be created with multisampling enabled)
Stream output buffer description configures the stream output stage of the pipeline trough D3D12_STREAM_OUTPUT_DESC .
Stream output stage is located between geometry and rasterization stages. It allows picking data as output from vertex or geometry shader, and making it available (recirculating it) for a subsequent render pass directly as input in the next vertex shader call.
This comes useful, for example, when we perform some geometry operation in vertex or geometry shader and the result to be also used for the next frame, as it happens with rendering of particles. This process will be much faster than store that data in a standalone buffer and reuploading it to GPU for the subsequent pass.
All the different structs describing the various parts of the PSO will serve to create the so called Pipeline State Description object of type D3D12_GRAPHICS_PIPELINE_STATE_DESC. From that, the PSO object is obtained by calling device->CreateGraphicsPipelineState(...)
method.
Thankfully, the helper library D3DX12 (available only on GitHub) will provide some helper structs to facilitate this whole process and make it less verbose, by providing another way to create the PSO.
This alternative way will use a D3D12_PIPELINE_STATE_STREAM_DESC type instead of the default PSO description object.
We are also going to need to use an ID3D12Device2 interface type of device.
We start by defining an object type called Pipeline State Stream, a struct made out of Tokens:
struct PipelineStateStream{
CD3DX12_PIPELINE_STATE_STREAM_ROOT_SIGNATURE pRootSignature;
CD3DX12_PIPELINE_STATE_STREAM_INPUT_LAYOUT InputLayout;
CD3DX12_PIPELINE_STATE_STREAM_PRIMITIVE_TOPOLOGY PrimitiveTopologyType;
CD3DX12_PIPELINE_STATE_STREAM_VS VS;
CD3DX12_PIPELINE_STATE_STREAM_PS PS;
CD3DX12_PIPELINE_STATE_STREAM_DEPTH_STENCIL_FORMAT DSVFormat;
CD3DX12_PIPELINE_STATE_STREAM_RENDER_TARGET_FORMATS RTVFormats;
} pipelineStateStream;
Each of those tokens represents an input data for the PSO description, from those described before. There are actually more tokens available, and the number will grow with the expansion of D3D12 features. A full list of tokens is available in the microsoft documentation website.
To define a pipeline state stream object, only a subset of tokens is required: for all the parameters that we don’t specify, the pipeline state stream will assign a default value. That is why we can select any number of tokens for our state stream, still the default PSO state is not enough to make the system work by itself: some components such as the vertex shader, are always needed. The order in which we declare tokens is not important.
After our pipeline state stream object type is defined, we create an object of that type and we fill all the fields with our data:
pipelineStateStream.pRootSignature = m_RootSignature.Get();
pipelineStateStream.InputLayout = { inputLayout, _countof(inputLayout) };
pipelineStateStream.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
pipelineStateStream.VS = CD3DX12_SHADER_BYTECODE(vertexShaderBlob.Get());
pipelineStateStream.PS = CD3DX12_SHADER_BYTECODE(pixelShaderBlob.Get());
pipelineStateStream.DSVFormat = DXGI_FORMAT_D32_FLOAT;
pipelineStateStream.RTVFormats = rtvFormats;
With the pipeline state stream description complete, we can now create the PSO object with it, of type ID3D12PipelineState, from the device with CreatePipelineState method:
D3D12_PIPELINE_STATE_STREAM_DESC pipelineStateStreamDesc = {
sizeof(PipelineStateStream), &pipelineStateStream
};
ThrowIfFailed(device->CreatePipelineState(&pipelineStateStreamDesc, IID_PPV_ARGS(&m_PipelineState)));
Note: When we need to change one of the attributes of the PSO, for example changing pixel shader the next frame, we are going to need to recreate the PSO object.
The PSO description contains D3D12_SHADER_BYTECODE fields for vertex, hull, domain, geometry and pixel shaders. This compiled shader data in binary form is obtained with the following steps:
Starting from HLSL code of our shader in a file MyShader.hlsl, we can generate the corresponding Compiled Shader Object (.cso). This object can be generated off-line or at runtime. We of course want to generate most of our compiled shader objects before running the application as much as possible for speed purposes.
Off-line compiling is done by using the standalone shader compiler executable FXC.exe that is included with the D3D12 SDK, with a command line like:
fxc /T ps_5_1 /Fo PixelShader1.cso PixelShader1.hlsl
Where ps_5_1 is the target profile (pixel shader model 5.1) followed by output file and input file. We can also specify additional flags like if to use debug information. This file can be loaded and automatically converted to binary code by using the function D3DReadFileToBlob.
Off-line compiling can be also done at compile time: Visual Studio can be configured to use the Effect Compiler Tool FXC.exe to automatically compile every file found in the solution that has extension “.hlsl” to a compiled shader object file “.cso”, that can be loaded in binary blob format at runtime by calling ReadData(“MyShader.cso”)
from a BasicReader object.
There is also the possibility to compile shaders and store the binary blob on byte array variables defined in header files.
Runtime compiling is done calling D3DCompileFromFile function that receives a path to the .hlsl file and returns the corresponding compiled binary blob directly.
Note: A real case scenario includes a system that stores the compiled shader’s binary blob in permanent memory, so that we just need to load it and link it to the PSO.
These additional settings are controlled by calling methods on the Command List, from a ID3D12GraphicsCommandList object (as described in the official documentation), rather than the PSO. This is because we can modify them on the fly without recreate the PSO, and use this last one for multiple command lists that do different tasks. For each of these settings that we do not specify, a default value will be assigned.
Vertex and Index buffers: the number and view interfaces of the vertex and index buffer that we want to bind as input for the input assembler in the current drawing pass.
This is done by calling IASetVertexBuffers
and IASetIndexBuffers
methods.
Stream output buffers: number and view interfaces of buffers that we want to bind for the stream output stage, done by calling SOSetTargets
method.
Render targets: number and view interfaces of our render targets that we want to write into, done with OMSetRenderTargets
method.
Descriptor heaps: number and resource views of the descriptor heaps (that are containers for resource views) that we want to bind for the current command operations, done with SetDescriptorHeaps
method.
Shader parameters: constant buffers, read-write buffers, and read-write textures, done with all SetGraphicsRoot
and SetComputeRoot
methods.
wherere $dv$ and $dw$ are render target’s units for width and height.
Note: We can obtain special effects, such as render everything in foreground (like flat objects for UI purposes) by setting MinZ=MaxZ=0 OR render everything in background (like a skydome) by setting MinZ=MaxZ=1.
Still, in most cases we want the range to cover the whole depth buffer spectrum (from 0 to 1) for our 3D models.
Scissor rectangles define areas inside the render target to consider valid for the output merger stage. This data is used for the Scissor Test, that will discard every pixel, computed by the pixel shader, that relies outside the bounds defined in scissor rectangles. It is notified to the command list by calling the RSSetScissorRects
method.
Constant blend factor sets the scalar value of the factor that will be optionally used during Blending and decided in the PSO Blend State description (when D3D12_BLEND::D3D12_BLEND_BLEND_FACTOR and/or D3D12_BLEND_INV_BLEND_FACTOR are used in the blending formulas). This value is set from the command list using the OMSetBlendFactor
method.
Stencil reference value is the default scalar value used when the stencil mode in the PSO is set to D3D12_STENCIL_OP::D3D12_STENCIL_OP_REPLACE.
Shader Specified Stencil Reference Value is a feature of D3D11.1 and 12 that allows to define a default stencil scalar value to use for the current draw, and that to be editable in a granular per-pixel manner inside the pixel shader, referring to the system value semantic SV_StencilRef. This mode will effectively write into the stencil buffer (and then used for the stencil test). The scalar value is set using the OMSetStencilRef
method from the command list.
commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);