The DeadLock came with the new UE 5.6.1 update and the reason are the UE changes in UObject* StaticAllocateObject(…) function, there are three new input arguments
int32 SerialNumber, FRemoteObjectId RemoteId, FGCReconstructionGuard* GCGuard of which for interest for us is FGCReconstructionGuard* GCGuard. The big change is on Line 3732 UObjectGlobals.cpp
if (ensureMsgf(IsInGameThread(), TEXT("GC lock can only be acquired on the game thread. If you hit this ensure then an object is being reconstructed on a worker thread which is not thread-safe")) && GCGuard)
{
GCGuard->Lock();
}
This will lock GCGuard and might block all threads waiting on GCGuard, including RenderThread.
Before final destruction the UObject must released all resources, rendering resources are released on RenderThread.
GameThread wants to be sure that all resources are released before the final destruction of the object and for that reason it sets a Rendering Fence. Fence is just a very simple task which should run on RenderThread and will be executed after all object resources are released.
In this case before releasing resources RenderThread executes render tasks which are using textures, to access these textures Renderer/RenderTask has to Pin texture WeakPtr. When WeakPtr is Pinned it calls
TStrongObjectPtr<UObject> FWeakObjectPtr::Internal_Pin(bool bEvenIfGarbage) const
{
FGCScopeGuard GCScopeGuard;
FUObjectItem* const ObjectItem = Internal_GetObjectItem();
return TStrongObjectPtr<UObject>(((ObjectItem != nullptr) && GUObjectArray.IsValid(ObjectItem, bEvenIfGarbage)) ? (UObject*)ObjectItem->GetObject() : nullptr);
}
The FGCScopeGuard constructor will call FGCCSyncObject::Get().LockAsync(); which will stay in infinite waiting because FGCCSyncObject waits to be signaled from the GameThread, this waiting is blocking the task execution on the RenderThread. As result our Fence set from the GameThread will never be executed and with that GamThread will stay in infinite While loop
while (!Obj->IsReadyForFinishDestroy())
{
// If we're not in the editor, and aren't doing something specifically destructive like reconstructing blueprints, this is fatal
if (!bPrinted && !GIsEditor && FApp::IsGame() && !GIsReconstructingBlueprintInstances)
{
StallStart = FPlatformTime::Seconds();
bPrinted = true;
}
FPlatformProcess::Sleep(0);
}
Obj->IsReadyForFinishDestroy() will never return True because the render fence task will never be executed on RenderThread