Cook nondeterminism in UStaticMeshComponent::BodyInstance

While investigating cook nondeterminism, I’ve stumbled upon an intermittent diff in the fields of UStaticMeshComponent::BodyInstance structure. Debugging that, I’ve seen that the contents of this field have a complicated fate (in my example, I have a level which contains several instances of a particular static mesh):

  • First, the normal deserialization happens (in this example, component has BodyInstance.CollisionEnabled == NoCollision)
  • Immediately after, UPrimitiveComponent::Serialize calls UPrimitiveComponent::Serialize -> FBodyInstance::LoadProfileData; in this example the CollisionProfileName field is set to “BlockAll”, so this field is overwritten by CollisionEnabled = QueryAndPhysics
  • Later, just before the package is saved for cooking, there’s a chain of calls that ends up executing OnRegister for the component; this calls UStaticMeshComponent::UpdateCollisionFromStaticMesh. If the static mesh is already compiled (this may or may not happen, depending on timings of async operations), this then calls FBodyInstance::UseExternalCollisionProfile (passing the BodySetup from the StaticMesh to the StaticMeshComponent) - in this example the external collision profile is “NoCollision”, which sets the field back to CollisionEnabled = NoCollision
  • At runtime, this isn’t a problem - because when mesh compilation finishes, UStaticMeshComponent::PostStaticMeshCompilation calls UpdateCollisionFromStaticMesh again. However, while cooking, the serialization for cook happens immediately, there is no wait.

I think there is a conceptual problem here, in that there are multiple sources of truth that don’t agree with each other:

  • for CollisionEnabled field, that’s the serialized value of the field and the value from the collision profile
  • for the entire BodyInstance field of the component, that’s serialized value of the component and the value in the mesh it references
  • there doesn’t seem to be a mechanism to synchronize these values. Maybe resave commandlet can help, but I’m not confident (eg it probably won’t know how to wait for mesh compilation). Also, I don’t think there’s anything that propagates actual collision profile from the mesh to the component (and if there were no disagreement between profiles for mesh and component, there wouldn’t be an issue).

Ultimately, I don’t think it results in any runtime difference for these meshes (because the serialized values ultimately don’t matter, they get overridden on component registration) - but I can’t make them transient either (because in some other scenarios conceivably they could matter - eg. if profile is set to “custom”, fixup logic is not applied).

Some more discoveries on this one. First of all, I’ve found that actually there is a mechanism to delay the cook until the static mesh compilation finishes (UStaticMeshComponent::IsCachedCookedPlatformDataLoaded). It seems that the issue happens in opposite case - when mesh compilation is not needed. The problem is that ‘fixup’ is called during UStaticMeshComponent::Serialize, which happens during preload. By that point, there’s no guarantee that preload happened for the StaticMesh that the component refers to - and if it didn’t, then UStaticMesh would be constructed, but not deserialized yet. In this situation, the UStaticMesh would not have its body initialized, meaning that ExternalCollisionProfile would not be set up yet. Since the mesh compilation task is never created, we never get the PostStaticMeshCompilation call, which normally calls UpdateCollisionFromStaticMesh. This is what seems to create this non-determinism.

As a fix, I’ve locally added another UpdateCollisionFromStaticMesh() call to the end of the UStaticMeshComponent::PostLoad call (right after existing call to PrecachePSOs, which happens if mesh is already compiled). I think the fixup in Serialize can be removed too once it’s being done in PostLoad, but I’m not sure about this.

Hi Andrew,

As with your other ticket, apologies for the delay!

We’ll take a look into this next week for you as well.

Best

Geoff Stacey

Developer Relations

EPIC Games

Hi Andrew - this and the other ticket look to be superceded by the ticket which was opened a few weeks ago. Is that correct?