Is changing r.Nanite.Streaming.StreamingPoolSize at runtime production-safe?

This question was created in reference to: [Scaling Nanite StreamingPoolSize/VRAM usage via Scalability Settings or at [Content removed]

Hi,

We would like to expose the Nanite streaming pool size as a video quality setting, applied from the in-game pause menu without requiring an application restart.

We have seen previous guidance on this topic — which recommended avoiding changing the Nanite streaming pool size at runtime, and instead reducing overall scene memory pressure. However, that answer didn’t explain what actually goes wrong if you do change it at runtime, which is the part we need in order to make a decision.

From our reading of the code, the path looks deliberately supported rather than accidental:

- r.Nanite.Streaming.StreamingPoolSize is ECVF_RenderThreadSafe only — not ECVF_ReadOnly, unlike r.Nanite.Streaming.NumInitialRootPages and NumInitialImposters

- r.Nanite.Streaming.Debug.StreamingPoolResize defaults to 1

- FStreamingManager::ResizePoolAllocationIfNeeded() handles a changed MaxStreamingPages with a full teardown/rebuild (uninstall all streaming pages, RelocateHierarchy compute pass, root page relocation, and ResetStreamingStateCPU() which also drops in-flight PendingPages)

- There is an explicit r.Nanite.Streaming.ResetStreamingPool console command that triggers the same reset path

Question: is changing r.Nanite.Streaming.StreamingPoolSize at runtime safe in a shipping build? If it is not recommended, could you clarify what specifically breaks or is at risk? And if it is safe, are there constraints we need to respect — in particular an upper bound to clamp the user-facing range to, given that growing the streaming pool reduces the root pool ceiling (MaxRootPoolSizeInMB = MaxPoolSizeInMB - StreamingPoolSizeInMB) and there is a Fatal log plus a check(NumAllocatedRootPages >= NumRootPages) guarding it?

Shaorong

[Attachment Removed]

Hello,

Thanks for reaching out. Runtime resizing of the Nanite streaming pool is a real feature, not something that happens to work by accident. It was added in CL 35734956 under UE-203599, which is also where r.Nanite.Streaming.ResetStreamingPool came from, and it shipped in 5.5. So the answer to “is it safe” is yes, it works, and nothing gets silently corrupted as long as you stay inside the memory bounds of the hardware you are targeting. The RelocateHierarchy compute pass you describe inside ResizePoolAllocationIfNeeded is the old implementation and only exists in 5.5 and 5.6. We recently changed that in 5.8, so that code is gone.

So what happens when changing the streaming pool size?

Any change to MaxStreamingPages triggers a full flush of the streaming pool inside FStreamingManager::ResizePoolAllocationIfNeeded, which runs at the top of BeginAsyncUpdate on the render thread. Every resident streaming page is flushed, root pages are reuploaded to their new GPU page indices via the fixup system, and ResetStreamingStateCPU rebuilds the CPU-side arrays and drops any in-flight PendingPages. The visible consequence is that all Nanite geometry momentarily falls back to root pages, then restreams. If you want to see exactly what that looks like before you build any of this, run ShowFlag.NaniteStreamingGeometry 0 in your worst-case scene. That is the frame your players will see, followed by everything streaming back in against r.Nanite.Streaming.MaxPageInstallsPerFrame and r.Nanite.Streaming.MaxPendingPages.

Something that needs to be called out is that r.Nanite.Streaming.ReservedResources defaults to 0 and is read-only, so unless you have deliberately turned it on, you are on the non-reserved path. The engine comment on that branch is blunt about what it does:

Non-reserved resource path: Make new allocation and copy root pages over. Temporary peak in memory usage when both allocations need to be live at the same time.

Both the old and the new Nanite.StreamingManager.ClusterPageData buffers are alive across the copy pass, and the old one lingers in the render target pool for several frames after that. With a 512 MB streaming pool and

a few hundred MB of root pages, that buffer is close to a gigabyte, so the resize will cause a large spike in VRAM consumption. We have seen a licensee OOM on console from this general pattern: a large pool being reallocated

before the old one was released, while switching graphics modes during gameplay. Their fix was to stop allowing the change in level.

So what I would suggest you do is go down the reserved resource path, which avoids the second allocation entirely. It memmoves the root pages in place and commits or decommits physical pages instead of reallocating, so the transient spike goes away rather than being worked around. GRHIGlobals.ReservedResources.Supported is enabled on D3D12 at Tiled Resources Tier 2 and above, and on Vulkan, so it is available on essentially all PC DX12 hardware. The caveats are that

It is still flagged as experimental and that r.Nanite.Streaming.ReservedResources is ECVF_ReadOnly, so it is a startup config opt-in you would need to validate rather than something you can toggle alongside the setting.

An upper bound on memory

The whole ClusterPageData buffer, including streaming pages and root pages, is capped by GetMaxPagePoolSizeInMB. That returns 4095 on AMD and 2048 on everything else, further clamped by GRHIGlobals.MaxViewSizeBytesForNonTypedBuffer. Please do not hardcode a slider maximum based on a number you validated on one machine; a value that is fine on an AMD GPU can lead to a crash on an NVIDIA GPU.

Within that budget, MaxRootPoolSizeInMB is just MaxPoolSizeInMB minus StreamingPoolSizeInMB, so every MB you give the streaming pool is an MB taken off the root page ceiling. Streaming pages are 128 KB, root pages are

32 KB. There are three guards, and they do not all behave the same way in a shipping build. So the range you expose has to be clamped at runtime, not authored as a constant. You can roughly calculate an upper bound as such:

MaxStreamingPoolMB = PoolCeilingMB - (PeakRootPages * 32 / 1024) - headroom

Our General Recommendation

Feel free to test out making a change to the streaming pool size, but apply it on a level transition or behind a loading screen rather than live from the pause menu. That removes the flush from the player’s view, removes the IO burst from the moment the game is running, and puts the transient double allocation somewhere you already control the memory. If you do want it to apply live, turn on reserved resources first so the spike is not there to begin with. Either way, use a small

number of quantized steps rather than a continuous slider, so you are testing a handful of configurations rather than a range, and clamp the top of the range at runtime as above.

Bear in mind the pool has a floor worth respecting, too. Push it low enough and r.Nanite.Streaming.QualityScale kicks in and starts raising pixels per edge to keep the visible set in budget. Watch the streaming pool percentage and Quality Scale in stat nanitestreaming while you try to find the lower bound, and make sure you thoroughly profile your game to avoid any unintended OOM errors.

Let me know if that helps at all or if you have any further questions.

Best,

Tim

[Attachment Removed]

One more question: Earlier I saw this cvar: r.Nanite.Streaming.Debug.StreamingPoolResize and Streaming pool size is gated by this. It follows the term “debug” which is the reason we are hesitating at the beginning. So I assume this is not true anymore?

So while the cvars do have Debug in them, we’ve been able to resize the streaming pool since 5.5, so it’s safe to use in that sense.

And also, sounds like reserved mode should be a default setting if they avoid the mem spike at no cost; what’s the reason for toggling it on the fly?

We are still trying to improve the Nanite streamer, but we have not had the opportunity to dedicate time to it, as we have not been able to allocate engineering resources to it recently. We might be able to squeeze in some work for 5.9, although I cannot make any guarantees.

It would be helpful to hear what you are trying to accomplish by resizing the streaming pool. Are you hitting any VRAM issues in your build, and do you want to account for that?

[Attachment Removed]

Yeah, that cvar could help then, but I would make sure users can only pick from a set of sizes rather than fully setting arbitrary pool sizes. Did you try to optimize the expensive assets already? That would be the first thing I would do to improve the use of the streaming pool.

[Attachment Removed]

Thank you for the reply; this is totally makes sense now.

One more question: Earlier I saw this cvar: r.Nanite.Streaming.Debug.StreamingPoolResize and Streaming pool size is gated by this. It follows the term “debug” which is the reason we are hesitating at the beginning. So I assume this is not true anymore?

And also, sounds like reserved mode should be a default setting if they avoid the mem spike at no cost; what’s the reason for toggling it on the fly?

[Attachment Removed]

We’ve shipped the game in an earlier engine version with fixed fairly small streaming pool size. And we did see some Nanite popping issues lately due to both relative small poolsize and more expensive asset being import into the project. Though we probably won’t change it for min spec player, as I came across this cvar I have a feeling this could be an opportunity to have graphic settings for those Higherend GPU user?

[Attachment Removed]