Setup Mesh Pass

After culling, it is time to organize the mesh draw commands for each mesh pass and prepare for render function calls. Although we briefly discussed this process in From Mesh Draw Commands to RHI Commands, let's now delve into the details.

At the end of the culling part, we said the result is saved into MeshCommands. Let’s start from there.

Main process

image

This is a really complex part.

Consider shifting your focus from UPrimitiveComponent to FSceneProxy, FMeshBatch, and FMeshDrawCommands, with a per-pass view in mind.

MeshCommands has already been organized by pass. In the image, you can see a key-value map where the mesh pass serves as the key, and the value is an array of FVisibleMeshDrawCommand.

image

To represent a pass, we create FParallelMeshDrawCommandPass to hold the data, including the visible mesh draw command array. Inside this structure, we have FMeshDrawCommandPassSetupTaskContext to hold the setup-related data.

image

The worker thread performs the actual work in the DispatchPassSetup function. The contents include:

image
  1. Generating mesh draw commands for dynamic meshes.
  2. Applying view overrides to the mesh draw commands.
  3. Sorting the mesh draw commands.
  4. Setting up the draw commands for instances.

Steps 3 and 4 are the keys to dynamic instance merging, which we will discuss later.

Later, the renderer begins rendering. In the RenderPrepass function, it calls BuildRenderingCommands, which forces the render thread to wait for the worker thread to finish. Then, it builds the rendering commands and finally calls DispatchDraw.

image

Dynamic instance merging

Unreal engine 5.2 has a feature called Draw Call Merging, includes a dynamic instancing system. Please check the documents here: https://docs.unrealengine.com/5.2/en-US/mesh-drawing-pipeline-in-unreal-engine/#drawcallmerging

The Theory

image

As a very simple example, let's say we have 2 identical Cubes and we have two ways to draw them:

  • Generating two Draw Calls to draw each Cube separately.
  • Generating one Instanced Draw Call to draw both instances simultaneously.

The former method will be slower than the latter. If we have 1000 Cubes, there will be a significant performance difference between the two approaches.

The problem lies in how to design a system that can automatically identify multiple Draw Calls that can be merged into one "Instanced Draw Call".

Let's first consider a few examples that cannot be merged:

image

❌ If Cubes have different materials, we cannot merge them because we cannot switch shaders within a single Instanced Draw Call.

image

❌ If Cubes have different meshes, such as one Cube being subdivided on a curved surface, we cannot merge them.

Therefore, we should conclude the following pattern:

If a system is able to identify certain Draw Calls with the same Shader, the same Pipeline State, and the same Mesh, then this system should be able to automatically merge these Draw Calls into one Instanced Draw Call.

This is the implementation approach of Unreal Engine's Dynamic Instance Merging system.

Implementation Details

This image shows the instance merging timings:

image

Let’s see how this works by add another cube.

image
  • In the SortVisibleMeshDrawCommands step, the mesh draw commands are sorted by the SortKey and the StateBucketId
  • So for example, if we have the same two cubes, they will:
    • Have the same sort key, because the material, the vertex factory and the settings are the same.
    • Have the same StateBucketId because they are referencing the same cached mesh draw command inside Scene.CachedMeshDrawCommandStateBuckets . You can check the source code in the FCachedPassMeshDrawListContextDeferred::DeferredFinalizeMeshDrawCommands function,
  • So in the SortVisibleMeshDrawCommands step, the sort process will put the two mesh draw commands together.
  • In the step 4, it loops all the mesh draw command
    1. image
    2. If current StateBucketId is the same as the privous one
      • Increase the privous MeshDrawCommandInfo ’s NumInstances
    3. Otherwise, create a new MeshDrawCommandInfo
      • Set the privous StateBucketId as current StateBucketId .

The result:

image

We only have one draw call, but two instances.

Discussion

Unreal Engine's Dynamic Instancing is conservative. This means that unless it is absolutely safe, Unreal Engine tends to avoid merging instances.

In other words, if you want to trigger this mechanism as much as possible to reduce Draw Calls, you need to consciously merge Shaders, reuse Meshes, and so on.

More aggressive merging requires the use of Nanite, but we won't discuss it further here.