Organizing Pipeline Configuration

The rendering pipeline in Direct3D 11 is used by configuring all of the available options, then calling the desired draw call to push data through the pipeline and produce some output. In general, there are three major groups of pipeline configuration. These include:
  1. Input configuration - including all configuration of the input assembler
  2. General stage configuration - including everything between the input assembler and the output merger
  3. Output configuration - including all configuration of the output merger
In general, in Hieroglyph 3 the general stage configuration is handled by the MaterialDX11 class, while the output configuration is handled by a render view instance. The input configuration is actually bundled together with the draw call into the PipelineExecutorDX11 class. This article will discuss how to implement a customized PipelineExecutorDX11 subclass, which allows easily adding new rendering techniques to the existing entity based rendering system of Hieroglyph 3.

Designing a PipelineExecutorDX11

When designing a new pipeline executor, you should think about a few general areas. The first one is to think about how the rest of the pipeline must be configured in order to utilize this new class. The pipeline executor binds resources to the input assembler and then issues an appropriate draw call to invoke the entire pipeline to process the input data. There are a variety of different draw calls that can be used to invoke the pipeline, but in general they end up producing similar outputs when you look at the output of the input assembler.

For example, you can use non-indexed draw calls to provide input geometry to the pipeline. In this configuration, you would normally bind a single vertex buffer for input, then perform a ::Draw() call to push the data through the pipeline. This will generate the appropriate geometry (depending on the primitive topology setting and arguments to the call) in the input assembler and pass assembled vertices to the vertex shader.

However, you could also produce similar vertices / geometry with a ::DrawIndexed() call. In this alternative configuration, you typically bind a vertex buffer and an index buffer to the input assembler. After invoking the pipeline, data is pulled into the input assembler and produces the same vertices from our previous call – the only difference is that the primitive connectivity data is coming from different sources. In the ::Draw() version, the primitives are defined by the vertex order in the vertex buffer. In the ::DrawIndexed() version, the primitives are defined by the index order in the index buffer.

So the point of this discussion is this: two different methods of invoking the pipeline can produce the exact same results for the rest of the pipeline. So when you are designing your new pipeline executor, you can consider how the resulting object can be used with both existing material designs, or if it will require some new materials, or both.

Execution Efficiency

The second area to consider is regarding efficiency. For a particular set of data that you need to get into the pipeline for processing, there are several ways to do it – as we saw in our previous examples. You are given total freedom to configure the input assembler and to invoke the pipeline as you see fit. This includes performing multiple drawing sequences or whatever else. It is your responsibility to consider how memory consumption and bandwidth will be affected, as well as the ability for the GPU to continue humming along without any stalls.

Fortunately the generic design of the system should allow you to easily swap in and out different implementations of the pipeline executor classes. This should facility easier profiling of different configurations with a minimal impact on the rest of your application.

Multithreading

The third area to consider is how your pipeline executor will be used in a multithreading context. In Hieroglyph 3, the renderer is designed such that it can perform all operations serially on the immediate context, or it can operate in parallel and produce command lists on deferred contexts which are later consumed by the immediate context. Because of this ability to switch between modes, you must be careful to ensure that your pipeline executor does not modify any of its member data during the actual execute method.

This sounds simple at first, but it can easily lead to very difficult to reproduce bugs. For example, if you use an InputAssemblerStateDX11 object as a member variable of your pipeline executor implementation class, then you will probably have issues with it in a multithreading context. The reason is that if your class implementation is used in two different rendering passes simultaneously (which is often the case --> like in a shadow map + final rendering) and they use different input attributes* then you will have a race condition between these two draw calls. One or the other is likely to configure the input assembler object after it has already been configured on another thread, leading to a mismatch and probably a Direct3D error.

So the best way to implement this thing is to ensure that you don’t modify any object level data during the execute function!



(*) this is only possible when the two sets of attributes are a super and sub set of one another respectively.

Last edited Apr 3, 2012 at 1:43 AM by jzink, version 4

Comments

No comments yet.