Empty Dispatches in Nanite Shading Pass

Similar issues:

[Content removed]

[Content removed]

https://dev.epicgames.com/community/learning/knowledge-base/lpl4/unreal-engine-ue-5-6-x-most-common-rendering-issues#emptydrawbinsmayreduceperformanceonnvidiartx20serieshardware

Hello,

We are not understanding what we are seeing when we look at dispatches in the Nanite Shading Pass on the PC. The expectation is that when a Nanite object is completely culled, it has no consideration in the raster or shading pass; Work Graph should be removing the empty dispatch. However, even with r.Nanite.MaterialVisibility=1 and shader bundling being forced on (just to be “experimentally” thorough), we are still seeing empty dispatches in the shading pass.

Here’s a breakdown:

Two Nanite objects, one occluded

[Image Removed] Nanite Base Pass - 31 dispatches

Two Nanite objects, one offscreen

[Image Removed] Nanite Base Pass - 29 dispatches

Two Nanite objects, both onscreen

[Image Removed] Nanite Base Pass - 31 dispatches

Two Nanite objects, both offscreen

[Image Removed] Nanite Base Pass - 28 dispatches

By looking at the number of dispatches in the Nanite Base Pass, we can see that even when the Nanite object is behind another one, there is an empty dispatch being generated for it. When we turn on r.ShowMaterialDrawEvents, we can see the material name of the occluded object in ShadeGbufferCS. For the purpose of this test, the aforementioned data was captured with r.ShowMaterialDrawEvents=0 in case this was negating visibility in any way.

In the actual game, we are seeing thousands of empty dispatches, many of which are likely due to this issue. On lower end GPUs, this causes an incredible amount of GPU overhead due to command processor overhead, while it is somewhat masked on higher end GPUs purely due to hardware capabilities. In Graham Whitehall’s Nanite GPU Driven Materials, there is mention of using the Work Graphs introduced in SM 6.8, however, we can’t see evidence of this (DiscardResource) actually happening in ShadeGBufferCS on any NV cards (tested on 1080, 2080, 3080, 5090):

[Image Removed]

What does Epic recommend here?

Thank you!

[Attachment Removed]

Steps to Reproduce

  1. Build the game with r.Nanite.MaterialVisibility=1. Optionally with bundling on as well.
  2. Create a scene with a ground and two nanite objects:
    1. One should be larger than the other.
    2. The larger object should be in front of the smaller object when the map starts, completely blocking it.
    3. The objects should have unique materials with bUsedWithNanite set to true
  3. Trace the scene with the gpu profiler and look at dispatch counts for Nanite Shading Pass
    [Attachment Removed]

Hi,

>The expectation is that when a Nanite object is completely culled, it has no consideration in the raster or shading pass

>By looking at the number of dispatches in the Nanite Base Pass, we can see that even when the Nanite object is behind another one, there is an empty dispatch being generated for it

If an object is outside the frustum (and r.Nanite.MaterialVisibility=1 is set) then the draw/dispatch will be culled. Occlusion testing is different in that the test happens on the GPU, so the CPU doesn’t know ahead of time if the draw/dispatch will be necessary or not, so it has to always submits the draw/dispatch.

Workgraphs are a potential solution to this problem on PC, but they have never left the experimental stage. Last I heard there are other performance concerns with the current implementations. I don’t think we would generally recommend the experimental workgraph path at this point, but if you want to play around with it you can by enabling r.Nanite.AllowWorkGraphMaterials=1.

Wrt addressing the immediate problem, anything you can do to minimize the amount of unique materials that are loaded at once would help. In particular, be careful with things like Material Instance Dynamic (MICs) that turn materials unique. We have seen cases where even seemingly simple scenes end up with thousands for unique materials because of MICs. I would recommend auditing the list of materials that show up with r.ShowMaterialDrawEvents to see if what is there is reasonable and expected.

In practice, we see that most often there are not nearly as many unique shaders as there are unique materials. So the same shader ends up being dispatched many times with different bindings (textures, material parameters, etc.).

We have recently introduced Bindless shading/rasterization for Nanite as an experimental feature to allow draws with the same shader to be merged. In practice that can often be a ~10x reduction in draws.

The code is in 5.8, but we didn’t get far enough with testing to default enable it yet. We expect to enable it internally shortly after summer break. If you will be at or near 5.8, I think this is a much better bet than the workgraphs for PC.

On 5.8 you can enable the bindless Nanite path for a given platform by making sure these are set in the platform config:

BindlessConfiguration=Minimal

bEnableNaniteBindlessShading=True

bEnableNaniteBindlessRasterization=True

[Attachment Removed]

HI Rune, thanks so much for the detailed response. In testing with the workgraphs on, we encountered no instability (in a smaller test) and definitely less dispatch overhead resulting in higher performance, but as you stated, there are still many unique dispatches due to multiple bindings, as the materials usage has been done in a way to attempt to have fewer unique shaders but highly parameterized materials.

Is there a clear chain of CLs we could backport from 5.8 to bring the updated bindless implementation to 5.7?

[Attachment Removed]

Hi,

CL49922470 in UE5/Main in the main CL where bindless Nanite was introduced. With follow-up submits in 49992713, 52061677 and 52535952.

There has also been some related changes from RHI side that you will probably also want/need: 52112868, 52564020 and 52919012.

I think those should be the main ones. If something is missing, it will most likely changes from either me (rune.stubbe) or (christopher.waters).

[Attachment Removed]