Skip to content

Render Pipelines

Carsten Rudolph edited this page Jun 26, 2021 · 2 revisions

This page describes how render pipelines are implemented in LiteFX.

A render pipeline stores all state information, that can easily and quickly be set to configure the GPU to execute draw calls and process input data. A pipeline state describes the following static aspects (i.e. immutable after initial configuration):

  • Pipeline Layout
    • Shader Program
    • Descriptor Sets
  • Rasterizer: polygon mode, cull mode, cull order, depth/stencil state
  • Input Assembler: vertex and index buffer layout, primitive topology

Besides the static state, a pipeline also has a dynamic state. This state is set whenever the pipeline is bound, so you can change it on the fly:

  • Viewports
  • Scissors
  • Line Width †
  • Stencil Ref
  • Blend Constants

† Note that line width is only an emulated property, that is only supported in Vulkan.

Pipeline states are the core of every rendering application. They configure the graphics pipeline before submitting draw calls. Switching the pipeline state is not necessarily a low-cost operation on the hardware. In fact, much of the optimization potential from using modern graphics API's originates from making pipeline switches explicit. You should therefore minimize the amount of switches required. This can be done in different ways. For example you can rearrange your draw calls and bundle them together, so that all draw calls for one pipeline state are executed before switching to another one and doing the same. Additionally, you can parameterize your shaders to allow for better re-usability and minimize the need to switch pipeline states, because of changes in the shader program.

Render Pipelines

Render Pipeline Layout

The render pipeline layout describes the shader program and the descriptors (i.e. descriptor sets and push constants) that are used by the shaders to access buffer memory.

Shader Program

A shader program is composed from shader modules, which are binary representations of shader bytecode. DirectX uses DXIL, while Vulkan uses SPIR-V shaders. For more information on shaders and how to integrate them into your application, refer to the shader Module targets page.

Descriptor Sets

As the name suggests, descriptor sets are sets of descriptors. A descriptor can be seen as a GPU-pointer to buffer memory. The pipeline layout defines which descriptors are used and how they are related to a descriptor set by defining descriptor set layouts. A descriptor can point to a constant/uniform buffer, a storage buffer, a texture, an input attachment or a sampler. Descriptors and descriptor sets are handled in more detail here.

Rasterizer State

The rasterizer state configures the operations, that are used to convert primitives to fragments. Conceptually the rasterizer is executed after the vertex, geometry and tessellation stages. The most important properties of the rasterizer are:

  • Polygon Mode
  • Cull Order (i.e. Vertex Winding)
  • Cull Mode
  • Depth/Stencil state

The polygon mode simply describes how to draw primitives, i.e. if they should be drawn solid or in a wireframe representation. The cull order specifies, which side of a primitive is seen as front or back. It is determined by the direction into which the vertices are provided. The rasterizer can then decide to not pass the primitive fragments further down the pipeline, which improves performance. It therefor checks, if the primitive is facing towards or away from the camera. Which primitives to pass on is decided by the cull mode.

Depth/Stencil State

The depth/stencil state is a little bit more complex and describes three related aspects:

  • Depth Test
  • Depth Bias
  • Stencil State

The depth stencil state is used, even if the render pass is not configured with a depth/stencil target. Configuring such a target only means, that you want to store and later access the results of the depth/stencil buffer. Let's take a look at the individual aspects.

Depth Testing

Depth testing is a technique that allows the rasterizer to drop certain fragments. It does this by performing a depth test. The depth state is used to configure this test. It contains three properties:

  • Enable: a boolean switch, that indicates, if depth testing should be used.
  • Write: a boolean switch, that indicates, if the depth of a fragment (that passed the depth test) should be written to the depth buffer.
  • Operation: the operation that is used to compare the current depth of a fragment to the incoming fragments depth in order to decide whether or not to drop the fragment.
Depth Bias

The depth buffer is not a linear buffer, but instead uses an exponential representation in order to improve accuracy for fragments that are closer to the camera. This is, however, not ideal for some scenarios (for example shadow mapping). To influence the depth distribution function, depth bias can be used. It is described in more detail elsewhere.

Stencil Testing

Similar to depth testing, stencil testing can be used to decide on whether to drop certain fragments. The stencil test is therefore executed right after the depth test. A stencil buffer allows for more flexibility than a depth buffer. Basically, a stencil buffer contains a individual value for a fragment and the stencil test can be configured on how to react to this value. The stencil value is called stencil reference and is set on a render pipeline during runtime. The stencil test is described by the following properties:

  • StencilFailOp: the operation to apply to the stencil value, if the stencil test fails.
  • StencilPassOp: the operation to apply to the stencil value, if the stencil test passes.
  • DepthFailOp: the operation to apply to the stencil value, if the depth test fails.
  • Operation: the operation that is used to compare the stencil value of the incoming fragment to the current value stencil buffer.

A stencil test can be configured to act for back or front faces. The stencil test configuration is stored within the stencil state, which contains the following properties:

  • Enable: a boolean switch to completely enable or disable the stencil test.
  • WriteMask: a mask to indicate which bits of the result stencil value to write to the stencil buffer.
  • ReadMask: a mas to indicate which bits to read from the current stencil buffer before applying any operation.
  • FrontFace/BackFace: the stencil test configuration for the front and back faces (see above).

Stencil buffers allow for a variety of applications. For example they can be used to differ which objects should receive which lighting in deferred shading contexts. Or they can be used to easily draw outlines around objects of a certain type. It all depends on how you want to use them.

Input Assembler State

The last important static state is the input assembler state. The input assembler is the process that is executed before any primitives are rendered. The input assembler states tells the GPU about the contents of the buffers to be drawn, most notably the vertex and index buffers. First of all, it dictates, how many vertices form a primitive. Currently only two flavors (strip and list) of three variants (points, lines and triangles) are supported. This behavior is controlled by setting the PrimitiveTopology.

Index Buffer Layout

The index buffer layout is pretty straightforward, as it only contains a single property: the size of each index in bytes. This can be either two or four bytes. Indices are always unsigned.

Vertex Buffer Layout

The vertex buffer layout describes the memory of a single vertex within the vertex buffer. A vertex property is also called attribute. Each attribute is bound to a location. In Vulkan this location can be specified directly, whilst in DirectX it is not used at all. Instead, DirectX uses semantics, which are a higher-level approach of specifying, what is expressed by the provided data. Conceptually, both approaches are equal in that they express where (from a shaders point of view) the data is read from.

Attributes have two more important properties: offset and format. The offset describes where the data for the attribute starts, relative to the memory of a vertex. Naturally it makes sense to use the C++ offsetof macro for this. The format specifies, how large the data is and how many elements it holds.

Dynamic State

The dynamic state is called dynamic, because it can change between individual draw calls. It is bound whenever a pipeline is used.

Viewports and Scissors

Viewports and scissors are similar in that they both can restrict the output to be only drawn to certain areas of the screen. If a fragment rendered by the pipeline is located outside this area, it can easily be discarded. They both are described by rectangles. Scissors are the easier of both, only describing a rectangle in which it is valid to draw. Viewports also describe a transformation, called viewport transform. A viewport transform scales down the result of the draw call to fit the viewport area.

Stencil Ref

The stencil reference is used to set the reference value for the stencil test. For more information, see the stencil state description above.

Blend Constants

Blend constants are used when render target blending is enabled on the render pass. The constants are only ever read, if the blend state of one or more render targets is configured to use constant blend factors.