Skip to content

Render Passes

Carsten Rudolph edited this page May 11, 2023 · 5 revisions

Render passes are the fundamental building block of a render graph, i.e., the structured flow of your application for computing a frame. This page describes how render passes are represented and implemented in LiteFX.

Render Passes

Defining Render Passes

From an abstract point of view, render passes in LiteFX are units of work for a GPU. They are recorded and executed sequentially and can be seen as a process, that writes into a set of render targets. Render targets from other render passes, that have been executed within the same frame, but before the current render pass can be read by shaders of the current render pass, if they are mapped as input attachments. Render passes can also render into persistent textures, that can be sampled later in the same or subsequent frames.

A render pass is defined as a child of a device:

auto renderPass = device->buildRenderPass(MultiSamplingLevel::x4)
    .inputAttachment(0, *firstPass, 0)
    .inputAttachment(1, *secondPass, 0)
    .renderTarget(RenderTargetType::Present, Format::B8G8R8A8_SRGB, { 0.f, 0.f, 0.f, 0.f }, true, false, false)
    .go();

Render Target Types

There are three major types of render targets, a render pass can render into. A maximum number of eight render targets per render pass is supported, though this limit is not strictly enforced by LiteFX (i.e. you can safely define more render targets, however you might receive an error from the driver or the debug layer, when trying to bind the targets during runtime).

Each render target is mapped to a location. A location may not be used twice for different render targets within the same render pass. Furthermore, the location range must be contiguous, i.e. there must not be any gaps in the locations.

Render targets have a format, which describes the memory layout of the image that backs the target. Note that not all combinations are valid. For example, you can naturally only use depth/stencil formats for depth/stencil render targets. Also only a limited number of formats is actually supported for present targets. You can use the getSupportedFormats method of the device swap chain to query the supported present target formats.

Each render target can be set to be volatile. If a render target is volatile, it's contents may be discarded after the render pass ends. You have to ensure, that the render target is not bound as input attachment to any other render pass in this case.

Color Targets

Color targets are the most common render target types. They can be cleared, if the clearColor parameter is set to true when defining the render target. The clearValue represents the color with which the render target will be cleared. The clearStencil property is ignored for this render target type.

Depth/Stencil Targets

For each render pass, there can only be one depth/stencil target defined. If the clearColor property is set to true, the target depth is cleared with the x channel of the clearValue vector. If the clearStencil property is set to true, the stencil (if supported by the format) is cleared with the y channel of the clearValue vector. This channel may then be set to a value outside the [0.0, 1.0] domain, that is usually used.

Currently only depth can be read from input attachments.

Present Targets

Present targets are similar to color targets, with the limitation that there can also be only one present target in a render pass. More importantly, there can also only be one present target over all render passes and the render pass that contains the present target, should be the last render pass to be executed, since ending it automatically causes a write to the current back buffer. It is possible to execute other render passes after presenting, but it is not possible to read from present targets or call another Present until the back buffer has been switched on the swap chain (using swapBackBuffer).

Mapping Input Attachments

You can map as many input attachments as you like (i.e. as the driver supports). A input attachment is accessed from a descriptor. When defining an input attachment, you specify the descriptor binding, the input attachment should be accessed from. Furthermore, you specify the source render pass, which forms an implicit dependency (though it is not necessarily checked, if the preceding render pass has been executed). Furthermore you have to specify the location of the render target in the preceding render pass, that is mapped to the descriptor binding.

It is a good practice to put all input attachments into their own descriptor set and use only one descriptor set for input attachments. For DirectX 12, the runtime automatically generates a sampler at slot 0, space 0 for input attachments, so make sure not to use this descriptor for other samplers when using input attachments. In Vulkan, no sampler is required to resolve input attachments.

Render Target Blending

Render targets contain a blend state, that defines how the result of a draw call is blended with the already existing contents of the render target image. Each render target has a default blend state and can be blended independently from each other. This can be used for a wide range of applications and is typically used to implement transparency effects. You can browse online for more in-depth information on alpha blending and blend states.

Using Render Passes

A render pass needs to be started and ended. You have to ensure to always end a render pass before starting another one, keeping the sequential execution order intact. When starting a render pass, you specify the back buffer that is used by the current frame. This sets the active frame buffer on the render pass, that should be used by the current back buffer for the frame.

// Swap the back buffers for the next frame.
auto backBuffer = m_device->swapChain().swapBackBuffer();

// Other potential render passes and other work
// ...

// Begin the render pass by swapping the frame buffer for the current back buffer.
renderPass->begin(backBuffer);

// Actual rendering on this render pass.
// ...

// End the render pass.
renderPass->end();

Multi-Sampling

A render pass can be initialized with a certain multi-sampling level. This level is then used for all render targets. It can be changed by calling changeMultiSamplingLevel. This causes the frame buffer to be reset. During this process, the frame buffer validates the specified number of samples for each render target format by calling the maximumMultiSamplingLevel method of the parent device. If the specified level is not supported an error will be thrown. To prevent this, check the maximum sampling level for each render target format beforehand and stick to the minimum of all. Note that DirectX 12 requires hardware to at least support 8 samples per pixel. Thus it should be also always valid to use up to x8 MSAA under Vulkan.

When using multi-sampling, the behavior of the render pass and frame buffer slightly changes. Instead of directly writing into the back buffer, a new image is created for the render target (which requires more memory). When ending the render pass, the image will be resolved into the single-sample back buffer. This ensures best performance by exploiting modern and efficient swap-chain models.