UE 5.8 Bug Report: NavDataRegistrationQueue null crash with streaming sub-levels
Description
Crash in UNavigationSystemV1::SpawnMissingNavigationDataInLevel when a streaming sub-level contains a saved RecastNavMesh actor. The NavDataRegistrationQueue accumulates null TObjectPtr<ANavigationData> entries that are never removed, causing ContainsByPredicate to dereference null.
Root Cause
-
A streaming sub-level has a
NavMeshBoundsVolumeand was built in the editor, saving aRecastNavMeshactor inside the sub-level. -
At runtime, when the sub-level is loaded,
ARecastNavMesh::PostLoadfires and callsRequestRegistration(), which callsRequestRegistrationDeferred(), adding the NavData toNavDataRegistrationQueue. -
Immediately after,
ARecastNavMesh::CheckToDiscardSubLevelNavDatadestroys the NavData because it is not part of thePersistentLevel(by design — sub-level NavData should be discarded). -
The destroyed NavData pointer remains in
NavDataRegistrationQueueas aTObjectPtr. After GC runs, the pointer becomes null. -
ProcessRegistrationCandidates(NavigationSystem.cpp:2775) encounters null entries but only skips them — it does NOT remove them:
ANavigationData* NavDataPtr = NavDataRegistrationQueue[CandidateIndex];
ULevel* OwningLevel = NavDataPtr != nullptr ? NavDataPtr->GetLevel() : nullptr;
if (OwningLevel && OwningLevel->bIsVisible)
{
// Process and remove
NavDataRegistrationQueue.RemoveAtSwap(CandidateIndex);
}
else
{
// Skip — null entries are NEVER removed
}
- When
PerformNavigationBoundsUpdateruns (triggered by NavMeshBoundsVolume registration duringAddToWorld), it callsSpawnMissingNavigationData()→SpawnMissingNavigationDataInLevel(), which callsContainsByPredicateon the queue:
const bool bIsWaitingForRegistration = NavDataRegistrationQueue.ContainsByPredicate(
[&NavConfig](const TObjectPtr<ANavigationData>& NavData)
{
return NavData->GetConfig().Name == NavConfig.Name; // CRASH: NavData is null
});
Reproduction Steps
- Create a persistent level (e.g.,
PersistentLevel). - Create a streaming sub-level (e.g.,
SubLevel) with aNavMeshBoundsVolume. - In the editor, build navigation for the sub-level. This saves a
RecastNavMeshactor in the sub-level. - Start PIE with the persistent level. Have the sub-level load at runtime (via
ULevelStreamingDynamicor Level Instance). - When the sub-level is added to the world (
AddToWorld), theNavMeshBoundsVolumeauto-registers viaOnNavigationBoundsUpdated. - On the next
NavSys::Tick,PerformNavigationBoundsUpdateruns. IfNavDataSetis empty (no NavData registered yet),SpawnMissingNavigationDatais called. SpawnMissingNavigationDataInLevelcallsContainsByPredicateonNavDataRegistrationQueue, which contains null entries from step 3’s destroyed RecastNavMesh.- Crash: null dereference in the lambda.
Call Stack
TArray::FindByPredicate() Array.h:1714
TArray::FindByPredicate() Array.h:1725
TArray::ContainsByPredicate() Array.h:1779
UNavigationSystemV1::SpawnMissingNavigationDataInLevel() NavigationSystem.cpp:4649
UNavigationSystemV1::SpawnMissingNavigationData() NavigationSystem.cpp:4549
UNavigationSystemV1::PerformNavigationBoundsUpdate() NavigationSystem.cpp:4387
UNavigationSystemV1::Tick() NavigationSystem.cpp:1651
UWorld::Tick() LevelTick.cpp:1647
Verbose Log Evidence
With log LogNavigation Verbose:
ARecastNavMesh::PostLoad RecastNavMesh ...SubLevel:PersistentLevel.RecastNavMesh-Default
UNavigationSystemV1::RequestRegistrationDeferred Adding to registration queue: RecastNavMesh ...SubLevel...
ARecastNavMesh::CheckToDiscardSubLevelNavData Discarding RecastNavMesh ...SubLevel... due to it not being part of PersistentLevel.
// Later, ProcessRegistrationCandidates repeatedly skips null entries:
UNavigationSystemV1::ProcessRegistrationCandidates Skipped None because owning level not visible.
UNavigationSystemV1::ProcessRegistrationCandidates Skipped None because owning level not visible.
// Then crash:
UNavigationSystemV1::PerformNavigationBoundsUpdate Add bounds Min=...