World composition level streaming hitch on mobile due to allocating landscape heightmap/weightmap textures

Hi,

I’m currently working on a 5.4.4 project targeting Android devices and we’re having some issues with level streaming hitches.

Some details about our setup:

- Android targeting Vulkan and GLES

- Using world composition, our world is split into 64 ‘tile’ levels and each of these has 16 LandscapeStreamingProxy actors.

- Using a lighting scenario to bake all lighting data from the tile levels into the persistent level build data, this was done to reduce the hitches when streaming in the tile levels caused by lightmap/shadowmap texture loading.

- We’re not using virtual texturing.

We still get some really large hitches during level streaming, and these are typically either from loading textures, or creating physics meshes. The textures are I think landscape heightmap and weightmap textures, one per LandscapeStreamingProxy which all seems to make sense, I’m just wondering if there is any advice as to what we can do about it? The cost appears to be mainly from allocating these texture mips on the render/RHI thread.

Have attached some screenshots from Unreal Insights trace file showing the 32 calls to FStreamableTextureResource_InitRHI on the render thread which takes 13ms, followed by 224 calls to Queue Submit on the RHI thread which takes 36.6ms. Each of those 32 FStreamableTextureResource_InitRHI allocates 7 times (for mips) which explains the 224 calls on the RHI thread.

Some questions:

1) Is our proxy setup wrong, and we should only have one LandscapeStreamingProxy per level? Is there any benefit to having more than one per level?

2) Heightmap/weightmap mips, can we or should we try reducing the mip counts?

3) Is there anything else we could try to minimize the time spent loading textures here, or a way to split it up, it seems quite excessive and this is on a fairly decent phone (Samsung S22 Ultra).

Thanks,

James

Steps to Reproduce

Hi James,

I don’t think there’s anything wrong with your setup although it does feel like you have a fairly big landscape for a mobile platform. How many landscape components per proxy do you have? For reference, in Fortnite, we are using 64 landscape proxies overall (with 4 components per proxy) and they are always loaded, while in your case, in a scenario where everything would be loaded, that would translate to 1024 landscape proxies (and N*1024 components depending on how many components per landscape proxy you have), which is quite a lot. Then depending on the number of target layers that are actually used on your components, the texture count can increase even more (on Fortnite, we have usually around 3 to 8 different layers on a given component, rarely more than this).

Texture-wise, here’s how things are laid out :

  • Weightmaps are shared between components of the same proxy, up to 4 per texture (R, G, B, A) but their resolution is == to the component’s resolution. That means, if you have, say, 4 components per proxy, and 3 layers ABC on component 0 and 1, and 2 layers AB on component 2 and 3, you have a total of 10 layers on your proxy, which requires 10 texture channels, and so you end up with 3 textures (i.e. 3 * 4 = 12 channels , 2 of which are unused)
  • Heightmaps can be shared geographically between neighboring components of the same proxy, up to 16 components, depending on the heightmap resolution, if I’m not mistaken. That means that if you have several components per proxy (and haven’t used the Add/remove component tool, which deactivates heightmap sharing), you might end up with a reduced number of heightmaps. TBH it’s a very annoying “feature” for the tooling part and we are planning to eventually get rid of it (we already don’t use it in the World Partition case), as it complicate things quite a lot and prevents some optimizations. I’m not sure if it’s active in the World Composition case, as it’s a code path that we don’t have the resources to exercise, since all developments are using World Partition now. Anyway, TLDR : at worst, you will have 1 heightmap per component, at best, 1 every N components.

It is not clear to me what you are actually loading at any given time. Are your levels loaded / unloaded dynamically? Like I said, for Fortnite, we’ve decided to have all landscape streaming proxies always loaded, as we want the player to always be able to see the entire landscape and we want collisions to always be present, even in the distance (otherwise, we might have considered HLOD, but even then, the gain in terms of memory is not great or even inexistent, as landscape textures are usually lighter than static meshes with unique textures on them). That isn’t to say that all the landscape data is loaded : texture streaming comes into play and mips are dynamically loaded/unloaded depending on the pixel ratio, just like any other streamable texture.

The fact that you seem to have 7 mips being loaded at a time is a bit suspicious, though, because when loading a far away landscape, only the low-resolution mips should be loaded, typically. Do you see those hitches on landscape textures belonging to proxies that are closed to the player only or do you see them also on faraway landscapes? If the latter, then your terrain textures might be mis-configured and the mip tail (i.e. the number of mips that are not streamed, i.e. always loaded) is too high?

Have a look at the texture group settings on your target platform and, in particular, the following 2 : TEXTUREGROUP_Terrain_Heightmap and TEXTUREGROUP_Terrain_Weightmap. We usually tweak LODBias, MaxLODSize, OptionalMaxLODSize, etc. to control the mips that should be cooke,d always loaded, and eventually, bias the LOD level on certain devices.

I suggest you use the command “listtextures -csv” at runtime to have a thorough analysis of what is loaded and evaluate if you don’t have too many mips loaded for the landscape proxies in the distance, for example.

Also, have a look at the number of layers that are currently in use because even if they are shared up to 4 per texture), that can boost the count quite a bit (especially if you use BP brushes to write those weights procedurally, as those will allocate the corresponding layer on all landscape components, regardess if they’re actually used on a given component or not). In order to validate this, you can use the “Layer Usage” landscape visualizer in the viewport options in the editor, it will show you the actual layer count on every landscape component. If it sounds excessive, try to reduce it, as it will lead to a lesser memory consumption but also a higher performance and simpler materials.

And also, if this is a situation you’re in and something that you can afford, you can try to reduce the number of conponents per proxy. As explained earlier, this number influences the sheer amount of texture objects and there decreasing it can reduce the strain on the streaming system and reduce the amount of draw calls too (there’s 1 draw call per component, no matter what). In terms of streaming “unit”, only the streaming proxy matters, since all components are loaded as part of the proxy, so having 16 components per proxy or a single one will lead to the same result.

Lastly, please consider your level loading situation : if you can afford not loading the faraway levels (e.g. by hiding them with fog or replacing them by HLOD), then it would result in a lesser amount of proxies/components/textures to load entirely, and that could yield a net gain.

Hopefully that gives you enough information to start investigating and try to how to improve your setup. Please don’t hesitate if you have more questions,

Cheers,

Jonathan

Hi Jonathan, thanks so much for the detailed response, that’s super helpful information.

To answer some questions, we have 64 ‘tile’ levels and each of those has 16 LandscapeStreamingProxies, so this means a total of 1024 streaming proxies and 1024 landscape components I believe. I’m a bit unclear how “landscape components per proxy” is determined or set, but it appears we have 1 per proxy.

Our landscape tile levels are streamed in/out by distance, and we have LOD1 versions of all of those too with LanscapeMeshProxyComponents instead. That means there are usually around 6-8 real landscape tiles loaded at a time (so 96 components/streamingproxies) and the rest will typically be loaded as proxy meshes, and the two types swap in/out as you move around.

Our layers look quite lightweight from what I can tell, at least it’s all green in the visualiser you mentioned.

The info on how weightmap/heightmap textures are shared is really useful, I think this is part of where we’re going wrong, having 16 streaming proxies per level and 1 component per proxy means we’re not benefiting from any of the weightmap/heightmap texture sharing you mention and we’re hitting the worst case for those. And having multiple streaming proxies per level doesn’t make sense to me as it’s the unit of level streaming.

I’m also going to investigate our texture group settings further as it’s clear from using listtextures that all mips are always being loaded and no texture streaming is happening with these!

Thanks again for the info, I’ll try making some of these changes and report back if I have any further questions.

To find out the number of components per proxy, you need to select a proxy in the scene outliner and there’s a section with this actor’s summary in the details :

[Image Removed]Since you seem to be conducting investigations on your own, I’ll close this ticket but feel free to re-open if you have other questions.

Cheers,

Jonathan