Actor Filtering With Nested Level Instances

Hiya!

We have noticed odd behavior locally. We are able to work around it, but wanted to confirm intent of the existing code and if there are other mechanisms we should be using to achieve this.

We have a large, complex World Partitioned world, with many levels instances throughout. Those level instances often contain nested level instances, and so forth. We also use a randomization system to vary the content of those level instances in the Editor; world artists and level designers can swap the variations of the level instances quickly to test different things, and then only see the “final selection” in PIE or a cooked game. We achieve this by filtering out actors via data layers, which has otherwise worked.

However, we started seeing odd behavior; disabled, filtered out variants were appearing alongside the correct ones. After some digging, I found the code in `FWorldPartitionStreamingGenerator::CreateActorDescViewMap` which handles this filtering. It would correctly filter out the immediate children of a given level instance, which often include filtering out child level instances. However, it would then proceed to recurse into the filtered-out level instances and keep their children around anyway. This matches the cases we were seeing “incorrect” behavior.

I was able to modify the engine locally pretty trivially to get the behavior we DO want, so we are not stuck on this issue. In our case, it is expected that a filter-out level instanced would implicitly filter-out all children.

This was unexpected for us, but appears to be intentional behavior. We were looking for guidance on if this is expected, and what might be a better way to achieve what we want.

Thanks!

Steps to Reproduce
Create a World Partition world

Create a Level Instance (“TopLevel”) which filters out some actors, for example via data layers and place it within this world

Create a Level Instance (“Nested”) which does not filter actors inside of the TopLevel instance, configured such that Nested would be filtered out

Create some actor within Nested (“TestActor”)

PIE or Cook, and observe TestActor included in the streaming level

Hello!

Can you provide a test project that demonstrate the problematic configuration? The provided steps are likely missing something as we haven’t been able to reproduce the problem.

Can you also share the fix that you found? It might help us understand what is happening.

Martin

The line number doesn’t match. Can you share the full function?

Thanks for sharing the function. We are now able to reproduce the problem and confirmed the proposed fix resolve it. We are continuing the investigation to see if there can be undesired side effects.

Hey Martin! Thanks for the response.

The fix is easier. We added this block of code within WorldPartitionStreamingGerenation.cpp line 1145 (which is the first line of the “Parse actor containers” for-loop):

				const TSet<FGuid>* const FilteredActorGuids = ContainerFilteredActors.Find(ContainerID);
				const bool bIsContainerActorFilteredOut = (FilteredActorGuids != nullptr) && FilteredActorGuids->Contains(ContainerCollectionInstanceView.GetGuid());
				if (bIsContainerActorFilteredOut)
				{
					continue;
				}

I don’t FULLY understand the code, but this appears to be working and is intended to find containers which have been filtered out by their parent’s rules, and avoid placing any of their children in the world.

I’ll try to get together a repro project for you as time allows over the next few days.

Sure:

	void CreateActorDescriptorViewsRecursive(FContainerCollectionInstanceDescriptor&& InContainerCollectionInstanceDescriptor)
	{
		// Inherited parent per-instance data logic
		auto InheritParentContainerPerInstanceData = [](const FContainerCollectionInstanceDescriptor& ParentContainerCollectionInstanceDescriptor, const FStreamingGenerationActorDescView& InActorDescView)
		{
			FContainerCollectionInstanceDescriptor::FPerInstanceData ResultPerInstanceData;
 
			// Apply AND logic on spatially loaded flag
			ResultPerInstanceData.bIsSpatiallyLoaded = InActorDescView.GetIsSpatiallyLoaded() && ParentContainerCollectionInstanceDescriptor.InstanceData.bIsSpatiallyLoaded;
 
			// Runtime grid is inherited from the main world if the actor has its runtime grid set to none.
			ResultPerInstanceData.RuntimeGrid = (ParentContainerCollectionInstanceDescriptor.ID.IsMainContainer() || ParentContainerCollectionInstanceDescriptor.InstanceData.RuntimeGrid.IsNone()) ? InActorDescView.GetRuntimeGrid() : ParentContainerCollectionInstanceDescriptor.InstanceData.RuntimeGrid;
 
			// Data layers are accumulated down the hierarchy chain, since level instances supports data layers assignation on actors
			ResultPerInstanceData.DataLayers = InActorDescView.GetRuntimeDataLayerInstanceNames().ToArray();
			ResultPerInstanceData.DataLayers.Append(ParentContainerCollectionInstanceDescriptor.InstanceData.DataLayers);
			ResultPerInstanceData.DataLayers.Sort(FNameFastLess());
 
			if (ParentContainerCollectionInstanceDescriptor.InstanceData.DataLayers.Num())
			{
				// Remove potential duplicates from sorted data layers array
				ResultPerInstanceData.DataLayers.SetNum(Algo::Unique(ResultPerInstanceData.DataLayers));
			}
 
			return ResultPerInstanceData;
		};
 
		// Hold on to ID
		const FActorContainerID ContainerID = InContainerCollectionInstanceDescriptor.ID;
		{
			TArray<FStreamingGenerationActorDescView> ContainerCollectionInstanceViews;
 
			// ContainerInstanceDescriptor may be reallocated after this scope
			{
				// Create container instance descriptor
				check(!ContainerCollectionInstanceDescriptorsMap.Contains(ContainerID));
 
				FContainerCollectionInstanceDescriptor& ContainerCollectionInstanceDescriptor = ContainerCollectionInstanceDescriptorsMap.Add(ContainerID, MoveTemp(InContainerCollectionInstanceDescriptor));
				
				// Gather actor descriptor views for this container
				CreateActorDescViewMap(ContainerCollectionInstanceDescriptor);
 
				// Resolve actor descriptor views before validation
				ResolveContainerDescriptor(ContainerCollectionInstanceDescriptor);
 
				// Validate container, fixing anything illegal, etc.
				ValidateContainerDescriptor(ContainerCollectionInstanceDescriptor);
 
				// Update container, computing cluster, bounds, etc.
				UpdateContainerDescriptor(ContainerCollectionInstanceDescriptor);
 
				// Calculate Bounds of non-container actor descriptor views
				check(!ContainerCollectionInstanceDescriptor.Bounds.IsValid);
				ContainerCollectionInstanceDescriptor.ActorDescViewMap->ForEachActorDescView([&ContainerCollectionInstanceDescriptor](const FStreamingGenerationActorDescView& ActorDescView)
				{
					if (ActorDescView.GetIsSpatiallyLoaded())
					{
						const FBox RuntimeBounds = ActorDescView.GetRuntimeBounds();
						// Test if RuntimeBounds is valid because GetIsSpatiallyLoaded() is affected by a valid ParentView
						// So the RuntimeBounds can be invalid in the case where its a non-spatial with a spatial parent.
						if (RuntimeBounds.IsValid)
						{
							ContainerCollectionInstanceDescriptor.Bounds += RuntimeBounds;
						}
					}
				});
 
				// Copy list as descriptor might get reallocated after this scope
				ContainerCollectionInstanceViews.Append(ContainerCollectionInstanceDescriptor.ContainerCollectionInstanceViews);
			}
 
			// Parse actor containers
			for (const FStreamingGenerationActorDescView& ContainerCollectionInstanceView : ContainerCollectionInstanceViews)
			{
// ENGINE CHANGE - [Content removed] are filtered out				
				const TSet<FGuid>* const FilteredActorGuids = ContainerFilteredActors.Find(ContainerID);
				const bool bIsContainerActorFilteredOut = (FilteredActorGuids != nullptr) && FilteredActorGuids->Contains(ContainerCollectionInstanceView.GetGuid());
				if (bIsContainerActorFilteredOut)
				{
					continue;
				}
// ENGINE CHANGE - [Content removed] are filtered out
				
				FWorldPartitionActorDesc::FContainerInstance SubContainerInstance;
				if (!ContainerCollectionInstanceView.GetChildContainerInstance(SubContainerInstance) || !SubContainerInstance.ContainerInstance)
				{
					continue;
				}
 
				FContainerCollectionInstanceDescriptor& ContainerCollectionInstanceDescriptor = ContainerCollectionInstanceDescriptorsMap.FindChecked(ContainerID);
				FContainerCollectionInstanceDescriptor SubContainerInstanceDescriptor;
 
				SubContainerInstanceDescriptor.ID = SubContainerInstance.ContainerInstance->GetContainerID();
				check(SubContainerInstanceDescriptor.ID == FActorContainerID(ContainerCollectionInstanceDescriptor.ID, ContainerCollectionInstanceView.GetGuid()));
 
				// @todo_ow: LevelInstance EDL Support
				// LevelInstance don't support Content Bundle containers nor EDL containers
				ensure(!SubContainerInstance.ContainerInstance->HasExternalContent());
				FStreamingGenerationContainerInstanceCollection SubContainerInstanceCollection({ SubContainerInstance.ContainerInstance }, FStreamingGenerationContainerInstanceCollection::ECollectionType::BaseAndEDLs);
				SubContainerInstanceDescriptor.ContainerInstanceCollection = MakeShared<FStreamingGenerationContainerInstanceCollection>(SubContainerInstanceCollection);
				SubContainerInstanceDescriptor.Transform = SubContainerInstance.ContainerInstance->GetTransform();
 
				// @todo_ow: this is to validate that new parenting of container instance code is equivalent
				const FTransform ValidationTransform = SubContainerInstance.Transform * ContainerCollectionInstanceDescriptor.Transform;
				check(SubContainerInstanceDescriptor.Transform.Equals(ValidationTransform));
 
				SubContainerInstanceDescriptor.ParentID = ContainerCollectionInstanceDescriptor.ID;
				SubContainerInstanceDescriptor.OwnerName = *ContainerCollectionInstanceView.GetActorLabelOrName().ToString();
				// Since Content Bundles streaming generation happens in its own context, all actor set instances must have the same content bundle GUID for now, so Level Instances
				// placed inside a Content Bundle will propagate their Content Bundle GUID to child instances.
				SubContainerInstanceDescriptor.ContentBundleID = ContainerCollectionInstanceDescriptor.ContentBundleID;
				SubContainerInstanceDescriptor.InstanceData = InheritParentContainerPerInstanceData(ContainerCollectionInstanceDescriptor, ContainerCollectionInstanceView);
				SubContainerInstanceDescriptor.HLODLayer = ContainerCollectionInstanceView.GetHLODLayer().IsValid() ? ContainerCollectionInstanceView.GetHLODLayer() : ContainerCollectionInstanceDescriptor.HLODLayer;
				SubContainerInstanceDescriptor.bIsHLODRelevant = ContainerCollectionInstanceView.GetActorIsHLODRelevant() && ContainerCollectionInstanceDescriptor.bIsHLODRelevant;
 
				if (WorldPartitionSubsystem && ContainerID.IsMainContainer() && ContainerCollectionInstanceView.GetChildContainerFilterType() == EWorldPartitionActorFilterType::Loading)
				{
					if (const FWorldPartitionActorFilter* ContainerFilter = ContainerCollectionInstanceView.GetChildContainerFilter())
					{
						ContainerFilteredActors.Append(WorldPartitionSubsystem->GetFilteredActorsPerContainer(SubContainerInstanceDescriptor.ID, ContainerCollectionInstanceView.GetChildContainerPackage().ToString(), *ContainerFilter));
					}
				}
 
				CreateActorDescriptorViewsRecursive(MoveTemp(SubContainerInstanceDescriptor));
			}
		}
 
		// Fetch the versions stored in the map as it can have been reallocated during recursion
		FContainerCollectionInstanceDescriptor& ContainerCollectionInstanceDescriptor = ContainerCollectionInstanceDescriptorsMap.FindChecked(ContainerID);
 
		if (!ContainerID.IsMainContainer())
		{
			FContainerCollectionInstanceDescriptor& ParentContainerCollection = ContainerCollectionInstanceDescriptorsMap.FindChecked(ContainerCollectionInstanceDescriptor.ParentID);
			ParentContainerCollection.Bounds += ContainerCollectionInstanceDescriptor.Bounds;
		}
 
		// Apply per-instance data
		ContainerCollectionInstanceDescriptor.PerInstanceData.Reserve(ContainerCollectionInstanceDescriptor.ActorDescViewMap->Num());
		ContainerCollectionInstanceDescriptor.ActorDescViewMap->ForEachActorDescView([&ContainerCollectionInstanceDescriptor, &InheritParentContainerPerInstanceData](FStreamingGenerationActorDescView& ActorDescView)
		{
			const FContainerCollectionInstanceDescriptor::FPerInstanceData PerInstanceData = InheritParentContainerPerInstanceData(ContainerCollectionInstanceDescriptor, ActorDescView);
			ContainerCollectionInstanceDescriptor.AddPerInstanceData(ActorDescView.GetGuid(), PerInstanceData);
		});
	}

Thanks. I’ll wait for any updates regarding potential risks, but otherwise I’ll consider this answered :slight_smile: