Nanite landscapes, component counts and performance overhead

Hello!

We are currently optimizing our Nanite landscape setup (not Mesh Terrain, old style landscapes) and I’d like to get a better understanding of the data I’m seeing.

We need to have multiple smaller landscapes present in our scene. Because of that, we’ve been trying to reduce the amount of components per landscape to help with performance/ memory usage and tracking the impact of them. Unfortunately, it’s not exactly clear where the impact will be observed and we might be missing some places/expecting the impact in the wrong places.

So far we’ve seen that:

  • Each landscape component creates it’s own raster bin and shading bin. Having tessellation enabled on the landscape causes the raster bin amount multiply by ~8, e.g. 64 component landscape goes from 64 raster bins to 576. Empty shading bins cause empty ExecuteIndirect() calls similar to any other unique materials in the scene, staggering the actual work. And we’ve also seen that the same is happening with empty raster bins, which seems to contribute to our VisBuffer performance degrading in such scenes.
  • Each landscape component increases UObject memory footprint and (not fully confirmed, still verifying) also increases the size of the physics scene by inflating the AABB tree.

We haven’t seen any runtime CPU performance impact from the component count. We also haven’t seen any streaming related spikes, however it might be that we’ve missed them/ they got hidden by something else.

Having observed this, we’d like to verify what we’re seeing.

  1. Is the performance impact that we are observing for the visibility buffer correct? Does each landscape component always generate a unique material instance that (1) creates a separate shading bin and (2) needs to create it’s own raster bin? How does enabling tessellation affect the raster bin count (is it x8 per landscape component or something else)?
  2. Are the memory footprint observations correct?
  3. Are there any other places where we will feel the impact of having too many landscape components in our scenes?
  4. What would be a max adequate landscape component size? Where would we see the impact if we were to go over this size?

Thanks in advance!

[Attachment Removed]

Hi,

Apologies for the delay!

Is the performance impact that we are observing for the visibility buffer correct?

Yes, enabling tessellation on the landscape means it can no longer use the fixed-function bins for rendering and must now use more bins for programmable materials. The empty bins can have a negative impact especially on lower end hardware, but you need to measure this to see how much. Bundling helps with this on consoles and in 5.8 we added experimental bundle emulation using bindless which in our testing improved the impact of the empty ExecuteIndirects on lower end hardware. If you can give me an idea of what hardware you’re concerned about I might be able to provide better advice.

Does each landscape component always generate a unique material instance that (1) creates a separate shading bin and (2) needs to create it’s own raster bin? How does enabling tessellation affect the raster bin count (is it x8 per landscape component or something else)?

Correct, each landscape component has a unique weight map and thus a unique material instance.

The raster bin breakdown usually looks like

Fixed function bins: 16 (no voxels) or 32 (voxels)

Fallback: 1+ (if r.Nanite.AllowProgrammableDistances 1)

Landscape: 1 per component

Then double the total number of bins because there’s a main and post pass for culling unless you have that disabled with r.Nanite.Culling.TwoPass 0.

If you open a default world partition level and delete every landscape section except 1 and turn on NaniteStats you should see 42 total bins. 32 are for fixed function main and post passes, and the remaining 10 are 4 x 2 for the landscape material instances and 1x2 for fallbacks because r.Nanite.AllowProgrammableDistances is 1. If you set r.Nanite.Culling.TwoPass 0, you should just see 21 total, which might make it easier to see what’s going on.

One suggestion to improve tessellation performance is to try increasing the r.Nanite.DicingRate to 3 or 4 - this often has negligible visual difference and lowers the cost of tessellation, but don’t go too high (watch stat GPU) because that can negatively impact performance.

Are the memory footprint observations correct?

Additional landscape components will increase memory footprint, but you’ll want to test this with memreports to see how significant it is for your use case.

Our docs mention the some of the additional cost

…twice the amount of data is streamed. One set of data for Nanite streaming and another set for texture streaming. Enabling Nanite causes both sets of data to reside in memory.

What would be a max adequate landscape component size? Where would we see the impact if we were to go over this size?

I’m reaching out to some colleagues who are more familiar with this and whether it’s impacted by world-building considerations and not just rendering.

[Attachment Removed]

My colleague provided additional helpful details:

Does each (Nanite) landscape component increases UObject memory footprint and the size of the physics scene by inflating the AABB tree enough to be a concern?

There’s usually 1 Nanite landscape component per ALandscapeStreamingProxy so UObject memory footprint should be of little to no concern. There’s 1 such component for every 64 landscape components in a given ALandscapeStreamingProxy because 1 Nanite component holds exactly 1 static mesh, which can only own up to 64 mesh sections and we have one mesh section per landscape component, in order to assign the component’s material instance to the corresponding mesh section. Above 64 landscape components per proxy, another Nanite landscape component gets created to host the next 64 and so on.

This is purely a visual component so there’s no impact on physics or navigation. No distance field or Lumen card data either : standard landscape is still around so all of these are handled by it.

Are there any other places where we will feel the impact of having too many (Nanite) landscape components in our scenes?

If Nanite rasterization costs are too high, you should increase the size of your landscape components to minimize it and fit in your budget. Shader bundles on PC may help here, but in our testing, it primarily helps on lower end hardware.

What would be a max adequate landscape component size? Where would we see the impact if we were to go over this size?

The maximum resolution per component is 511x511 (255x255 quads + 2x2 subsections). Then landscape can be scaled as needed in order to maintain the desired vertex ratio per world unit.

Larger components have the benefit of minimizing the amount of landscape MICs, which limits the amount of draw calls for standard landscape and raster bins for Nanite landscape.

But it also increases the potential for more expensive materials, since landscape target layer usage will likely become higher which is a consequence of the landscape components being less granular. When using RUntime Virtual Textures (RVT), this cost is amortized since it will mostly affect RVT page rendering, which happens over the course of multiple frames, only when new pages are needed and on a limited render area.

There is no component count limit on a given landscape streaming proxy, so it’s up to you to find the balance. Larger landscape streaming proxies have the following benefits :

  • Tends to minimizes the cost of streaming (i.e. a lower actor streaming granularity means larger amount of data to stream at once but at a lesser frequency).
  • Allows to minimize the amount of Nanite landscape components, although this is usually of little importance, since what matters for Nanite is the amount of mesh sections (see above).

On the other hand, they have the following drawbacks :

  • All the CPU-resident data (landscape grass, if not using runtime grass generation, landscape collision, navigation data, Lumen cards, distance field data) will remain resident for longer, until the landscape proxy is replaced by HLOD
  • Workflow-wise, minimizes the potential for concurrent work. Landscape streaming proxy size is identical in-editor and in-game. In other words, since an artist works at the actor level, the streaming proxy granularity is what defines how artists will be able to work on different sections of the world at the same time

Some licensees, use smaller sizes allow multiple artists to work simultaneously, even using a single component per ALandscapeStreamingProxy. This means you end up with a single Nanite landscape component per proxy too. But if it’s in your budget it allows you to keep the actor streaming granularity low, and you can rely on HLOD in the distance to minimize system memory.

On Fortnite, we use on 4 components per proxy and we don’t use HLOD at all (landscape is non-spatially loaded).

Another thing to remember is that most the heavier landscape data (heightmaps/weightmaps/Nanite landscape mesh data) is already streamable and they participate to their respective shared video memory pool so there’s automatic balancing happening there. If memory/streaming is a concern, we recommend looking into using HLOD and reducing your landscape streaming actor granularity in order to save on collision, navmesh, landscape grass data, etc.

Runtime grass can be saved altogether by activating grass.GrassMap.UseRuntimeGeneration 1 which will cause it to be rendered at runtime as needed and only a minimal amount kept resident.

[Attachment Removed]

You’re welcome! If you have additional questions feel let us know, otherwise we can close this ticket thanks.

[Attachment Removed]

Hi Alex, thank you for the information and for the various breakdowns! It’s gonna be much easier to read the data now :blush:

I was doing general testing on a RTX 3080 to get a better understanding of VisBuffer timings in a complex scene (a lot of foliage, no masked materials). Then I was analyzing my capture in PIX and this was when I noticed a lot of empty ExecuteIndirects happening in VisBuffer with most of them coming from landscape related shading bins (increased by having tessellation on) - and it looked like that they had the biggest impact on VisBuffer being slower than expected.

BasePass was of course affected as well with back-to-back empty ExecuteIndirect dispatches - but this was expected and known of.

For tessellation I have already set r.Nanite.DicingRate before enabling it which helped a lot. I’m also using tessellation fade distances and a minimal displacement amount value as recommended. From what I’ve seen, the tessellation performance itself has been okay-ish, it’s the empty raster bins (seemingly) choking the VisBuffer performance that made me heavily consider if we can allow it for our setup where we get more landscape components in our scene than a typical scenario.

[Attachment Removed]

Thank you! This is a lot of great information and good insight :blush:

[Attachment Removed]

Thank you! I think we can close this ticket, we have enough information to move forward, really appreciate it :slightly_smiling_face:

[Attachment Removed]