Skip to main content

Streaming and budgets

Why this matters

Every streaming system in this folder — World Partition cells, Data Layer state, HLOD swaps, PCG runtime generation — ultimately competes for the same thing: a per-frame budget of loading, decompression, and memory bandwidth. A design that's individually correct (right cell size, right HLOD layer, right Data Layer toggles) can still hitch in practice if too many of those systems ask for work on the same frame. Understanding streaming sources and how loads get prioritized is what turns "it works in the editor" into "it doesn't hitch when the player sprints toward a cell boundary."

Mental model

Streaming is fundamentally a race between how fast a source moves through the world and how fast content can load in response. Every budget conversation in this document is about managing that race: how far ahead you predict (loading range), how much you're willing to block for it (blocking vs. async), and how much load you avoid needing in the first place (HLODs, Data Layer discipline).

The mechanics

Streaming sources drive everything

As covered in World Partition, a streaming source is anything implementing IWorldPartitionStreamingSourceProvider — most commonly a UWorldPartitionStreamingSourceComponent on a pawn. Each source reports its location, and optionally its shape and priority (FWorldPartitionStreamingSource), so the runtime hash can decide which cells to request. A source can mark itself as should-block-on-slow-streaming, which changes the failure mode when loading falls behind: blocking sources stall the game to guarantee content is ready; non-blocking sources let the player run ahead of fully-loaded content and accept visible pop-in instead.

Multiple simultaneous streaming sources (split-screen, a spectator plus a player, cinematic cameras) each pull their own loading radius — every additional source is additional concurrent streaming demand, not free.

Legacy level streaming volumes

Pre-World-Partition streaming (and still relevant for smaller maps or sublevel-based projects) uses ALevelStreamingVolume actors: a player standing inside a volume's bounds triggers load/unload of the sublevel(s) it's associated with. UWorld::ProcessLevelStreamingVolumes is what evaluates local players against these volumes each relevant tick. The Level Details window (per streaming sublevel) exposes performance-relevant settings like minimum unload request time, which prevents a level from being loaded and unloaded repeatedly if a player oscillates near a volume's boundary.

note

Legacy ALevelStreamingVolume-based streaming and World Partition's runtime hash are different systems — volumes don't apply to World Partition maps, and mixing the two mental models on the same project is a common source of confusion. Confirm which streaming model a given map actually uses before reasoning about its budget.

Where budget actually goes

Cost centerWhat drives itPrimary lever
Cell loadingGrid cell size, loading range, actor density per cellCell size, HLODs at distance
Data Layer transitionsHow many actors flip Unloaded ↔ Activated per eventBatch transitions; avoid per-frame toggling
HLOD swapDistance thresholds, outdated HLOD countKeep HLODs built and current (GetNumOutdatedHLODActors)
PCG runtime generationGeneration trigger, generation source movementGenerate-on-load for static content; scope runtime triggers narrowly
Async load queueTotal concurrent streaming sources, asset size/compressionReduce simultaneous sources; prioritize by source

Watching it happen

Diagnosing a hitch starts with knowing whether it's a loading-queue problem (too much requested at once) or a main-thread problem (too much synchronous work processing what loaded). stat levels and stat streaming-style stat commands (exact names vary by engine version) surface pending streaming requests and level state, which is the first place to look before assuming a specific cell or Data Layer is at fault.

Reacting to HLOD staleness as a coarse budget signal
void AWorldBudgetMonitor::CheckHLODHealth()
{
if (const UWorldPartitionHLODRuntimeSubsystem* HLODSubsystem =
GetWorld()->GetSubsystem<UWorldPartitionHLODRuntimeSubsystem>())
{
if (HLODSubsystem->GetNumOutdatedHLODActors() > 0)
{
UE_LOG(LogTemp, Warning, TEXT("Outdated HLODs present — distant content may be loading full detail instead."));
}
}
}

Gotchas

Blocking streaming sources hide budget problems until they don't

A blocking source (bBlockOnSlowStreaming-style behavior) makes streaming look fine in normal play because it stalls rather than pops content in late. That stall is still a hitch — it just happens as a frame-time spike instead of visible pop-in. Don't mistake "no visible pop-in" for "no streaming cost."

Outdated HLODs mean you're paying full detail cost at distance

If HLODs aren't rebuilt after content changes, the runtime subsystem may fall back to full-detail content further than intended, quietly blowing the budget the HLOD layer was supposed to protect. Rebuilding HLODs is part of the content pipeline, not a one-time setup step.

Data Layer toggling in bulk is a streaming event, not a flag flip

Activating a Data Layer that tags thousands of actors is functionally a large streaming request. Batch and stagger large Data Layer transitions the same way you would think about loading a new sublevel, rather than treating SetDataLayerRuntimeState as an instantaneous call.

See also