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.
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.
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.
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):
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.
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);
});
}