Cook non-determinism in UStaticMeshComponent generated by construction scripts

There’s a problem with how UStaticMeshComponent interacts with construction scripts:

  • AActor::RerunConstructionScripts has a logic to preserve properties of existing components generated by construction script, then delete & recreate them, and reapply preserved properties
  • To do that, for SM components it calls UStaticMeshComponent::GetComponentInstanceData, which creates a FStaticMeshComponentInstanceData structure, which serializes the component using special archive FDataCachePropertyWriter
    • For the StaticMesh property, this archive creates a copy (by calling DuplicateObject) and stores this copy in its internal list of references
    • FStaticMeshComponentInstanceData also stores the original version of the static mesh in its StaticMesh field
  • After deleting components and recreating them by rerunning construction script, engine calls FComponentInstanceDataCache::ApplyToActor
    • The first interesting thing happens in FActorComponentInstanceData::ApplyToComponent, which uses FComponentPropertyReader to restore properties - this sets the StaticMesh property of the recreated component to the ‘duplicate’ version of the static mesh
    • The derived class then calls UStaticMeshComponent::ApplyComponentInstanceData, which does an early-out if this condition is true: GetStaticMesh() != StaticMeshInstanceData->StaticMesh. Since the StaticMesh of the component is a duplicate mesh (set earlier by the base class) and StaticMeshInstanceData stores the original mesh, this condition is always true - so we always early out and skip all the remaining logic.

One observable consequence of this is the cook non-determinism: when UStaticMesh::PostDuplicate generates unique random guid for LightingGuid property (which is marked as deprecated, but is still serialized) - and since it’s this copy that ultimately gets stored in the cooked package, we have a new unique guid every cook. The remaining fixup logic in UStaticMeshComponent::ApplyComponentInstanceData is also always skipped, which doesn’t seem to be intended.

I’ve made a local change to replace this check+early-out with SetStaticMesh(StaticMeshInstanceData->StaticMesh). This fixed the non-determinism, but I wonder whether that will have any unintended consequences?

Steps to Reproduce

Hey Andrew, apologies for taking so long to respond. We’ve almost caught up on questions asked during Epic’s office closures so our response times will be back to normal again.

I’m trying to achieve a repro of the behavior you describe but I need some more info to achieve that. You mentioned:

“For the StaticMesh property, this archive creates a copy (by calling DuplicateObject) and stores this copy in its internal list of references”

Are you observing that a UStaticMesh is being duplicated? If so, that’s not behaving as I would expect. I can’t get this to happen locally in my UE 5.5.4 project, so if you are seeing this: can you share a callstack where DuplicateObject() is being entered as part of FDataCachePropertyWriter?

“The derived class then calls UStaticMeshComponent::ApplyComponentInstanceData, which does an early-out if this condition is true: GetStaticMesh() != StaticMeshInstanceData->StaticMesh”

Since this is a downstream effect of the unexpected mesh duplication, I suspect that this problem will go away when we figure out why that mesh duplication takes place.

It’s possible that I need to do more steps to mimic your setup to observe the bug. That DuplicateObject callstack would help. In addition:

  • Is the StaticMeshComponent defined in C++ (CreateDefaultSubobject), SCS (added in BP in the hierarchy) or UCS (blueprint Construction Script graph)?
  • Is the actor that fails the GetStaticMesh() != StaticMeshInstanceData->StaticMesh check an actor instance placed in the map, or a blueprint class?
  • Is that actor’s class a child blueprint?

The additional info may help me repro. If you have a small repro project, that would be very helpful too. Thanks!

Hi, sorry I gave up on this thread and didn’t see the answer :slight_smile:

Yes, the `UStaticMesh` is duplicated - here’s the stack:

>	UnrealEditor-CoreUObject.dll!StaticDuplicateObjectEx(FObjectDuplicationParameters & Parameters) Line 3295	C++
 	[Inline Frame] UnrealEditor-CoreUObject.dll!StaticDuplicateObject(const UObject *) Line 3158	C++
 	UnrealEditor-CoreUObject.dll!DuplicateObject_Internal(UClass * Class, const UObject * SourceObject, UObject * Outer, const FName Name) Line 4674	C++
 	[Inline Frame] UnrealEditor-Engine.dll!DuplicateObject(const UObject *) Line 1866	C++
 	UnrealEditor-Engine.dll!FDataCachePropertyWriter::GetDuplicatedObject(UObject * Object) Line 82	C++
 	UnrealEditor-Engine.dll!FDataCachePropertyWriter::operator<<(UObject * & Object) Line 125	C++
 	UnrealEditor-CoreUObject.dll!FArchiveUObject::SerializeObjectPtr(FArchive & Ar, FObjectPtr & Value) Line 91	C++
 	UnrealEditor-Core.dll!FStructuredArchiveSlot::operator<<(FObjectPtr & Value) Line 288	C++
 	[Inline Frame] UnrealEditor-CoreUObject.dll!ObjectPtr_Private::Friend::SerializePtrStructured(FStructuredArchiveSlot) Line 768	C++
 	[Inline Frame] UnrealEditor-CoreUObject.dll!operator<<(FStructuredArchiveSlot) Line 845	C++
 	UnrealEditor-CoreUObject.dll!FObjectProperty::SerializeItem(FStructuredArchiveSlot Slot, void * Value, const void * Defaults) Line 197	C++
 	UnrealEditor-CoreUObject.dll!FPropertyTag::SerializeTaggedProperty(FStructuredArchiveSlot Slot, FProperty * Property, unsigned char * Value, const unsigned char * Defaults) Line 583	C++
 	UnrealEditor-CoreUObject.dll!UStruct::SerializeVersionedTaggedProperties(FStructuredArchiveSlot Slot, unsigned char * Data, UStruct * DefaultsStruct, unsigned char * Defaults, const UObject * BreakRecursionIfFullyLoad) Line 2003	C++
 	UnrealEditor-CoreUObject.dll!UStruct::SerializeTaggedProperties(FStructuredArchiveSlot Slot, unsigned char * Data, UStruct * DefaultsStruct, unsigned char * Defaults, const UObject * BreakRecursionIfFullyLoad) Line 1446	C++
 	UnrealEditor-Engine.dll!UStruct::SerializeTaggedProperties(FArchive & Ar, unsigned char * Data, UStruct * DefaultsStruct, unsigned char * Defaults, const UObject * BreakRecursionIfFullyLoad) Line 549	C++
 	UnrealEditor-Engine.dll!FDataCachePropertyWriter::SerializeProperties() Line 39	C++
 	UnrealEditor-Engine.dll!FComponentPropertyWriter::FComponentPropertyWriter(const UActorComponent * Component, FActorComponentInstanceData & InInstanceData) Line 185	C++
 	UnrealEditor-Engine.dll!FActorComponentInstanceData::FActorComponentInstanceData(const UActorComponent * SourceComponent) Line 492	C++
 	UnrealEditor-Engine.dll!FSceneComponentInstanceData::FSceneComponentInstanceData(const USceneComponent * SourceComponent) Line 3080	C++
 	UnrealEditor-Engine.dll!FPrimitiveComponentInstanceData::FPrimitiveComponentInstanceData(const UPrimitiveComponent * SourceComponent) Line 782	C++
 	UnrealEditor-Engine.dll!FStaticMeshComponentInstanceData::FStaticMeshComponentInstanceData(const UStaticMeshComponent * SourceComponent) Line 71	C++
 	[Inline Frame] UnrealEditor-Engine.dll!TStructOnScope<FActorComponentInstanceData>::InitializeAs(const UStaticMeshComponent * &&) Line 185	C++
 	[Inline Frame] UnrealEditor-Engine.dll!MakeStructOnScope(const UStaticMeshComponent * &&) Line 376	C++
 	UnrealEditor-Engine.dll!UStaticMeshComponent::GetComponentInstanceData() Line 3088	C++
 	UnrealEditor-Engine.dll!FComponentInstanceDataCache::FComponentInstanceDataCache(const AActor * Actor) Line 603	C++
 	[Inline Frame] UnrealEditor-Engine.dll!FActorTransactionAnnotation::Create(const AActor *) Line 426	C++
 	UnrealEditor-Engine.dll!AActor::RerunConstructionScripts() Line 369	C++
 	UnrealEditor-Engine.dll!ULevel::IncrementalRunConstructionScripts(bool bProcessAllActors) Line 1946	C++
 	UnrealEditor-Engine.dll!ULevel::IncrementalUpdateComponents(int NumComponentsToUpdate, bool bRerunConstructionScripts, FRegisterComponentContext * Context, unsigned char VisibilityGroup, bool bActorWarmUpForStreaming) Line 1804	C++
 	UnrealEditor-Engine.dll!ULevel::UpdateLevelComponents(bool bRerunConstructionScripts, FRegisterComponentContext * Context) Line 1627	C++
 	UnrealEditor-Engine.dll!UWorld::UpdateWorldComponents(bool bRerunConstructionScripts, bool bCurrentLevelOnly, FRegisterComponentContext * Context) Line 2983	C++
 	UnrealEditor-UnrealEd.dll!UEditorEngine::InitializePhysicsSceneForSaveIfNecessary(UWorld * World, bool & bOutForceInitialized) Line 4341	C++
 	UnrealEditor-UnrealEd.dll!UEditorEngine::Save(UPackage * InOuter, UObject * InAsset, const wchar_t * Filename, const FSavePackageArgs & InSaveArgs) Line 4451	C++

Unfortunately, I don’t have a small project repro - this is all found while trying to make a large game cook deterministically.

Interestingly, I can’t reproduce it anymore, after we’ve updated to 5.6 and resaved all assets.

So digging a bit more:

  • The StaticMeshComponent couldn’t have been created by UCS (because otherwise UActorComponent::IsEditableWhenInherited would have returned false and FActorComponentInstanceData would not have run the serialization at all).
  • However, since FComponentInstanceDataCache did call GetComponentInstanceData, then either component must have been created by simple construction script or the entire actor should have been created by a child-actor-component.
  • Now, the interesting bit - if I create a very simple blueprint with a SM component (so CreationMethod == SimpleConstructionScript), and modify the mesh in the blueprint instance on the level (this is needed so that SerializeVersionedTaggedProperties does not skip the property as matching archetype), then FDataCachePropertyWriter::operator<<(UObject*&) is called (same as in the stack i’ve seen before) - except that it doesn’t duplicate the mesh, since the component is not an outer of the mesh.

So judging by the stack I’ve seen before, somehow we’ve got into a situation where StaticMeshComponent is an outer of the StaticMesh. Looking through my old notes, the mesh has ‘generated’ in its name, so this looks like a good lead. Unfortunately, this was more than a month ago, and now the same levels cook without triggering this codepath…

“somehow we’ve got into a situation where StaticMeshComponent is an outer of the StaticMesh”.

“the mesh has ‘generated’ in its name, so this looks like a good lead.”

That’s good info indeed. I’m not sure when a UStaticMesh would get outered to a UStaticMeshComponent. I’ll ask internally if someone has an idea when that can arise.

Now, since you updated the engine version and the bug doesn’t happen anymore I’m inclined to leave this issue be. It may have been fixed in the meantime. Either way, I’ll keep your report in mind if under people run into this as well. Does that sound alright with you as well?

If you do run into this after all in 5.6, do let us know!

Thanks!

We weren’t able to come up with theories for how a StaticMesh can get StaticMeshComponent as an outer. If you ever find the cause for this, we’d love to know! Until then, or unless the issue happens again, we won’t investigate further. Glad to hear that the problem seems to have stopped occurring in 5.6 though.