We found an issue with the Widget Component when you set it’s collision type to ‘No Collision’, then when the level containing the actor is streamed in and out, the level is unable to fully unload, causing a GC crash as the level cannot be deleted. (see attached ‘callstack’ showing the GC chain)
We found that even though the widget component has ‘No Collision’ set that it would still create a BodySetup and as can be seen the reason the level can’t be GC’d is because the BodySetup has the ‘Async’ flag on it. Upon further investigation we found that a BodySetup is always created for a WidgetComponent via BeginPlay.
However the reason for the BodySetup having the Async flag appears to be from FWidget3DSceneProxy, as this object is created on the Render thread, the FWidget3DSceneProxy constructor’s call to GetBodySetup actually creates the BodySetup. As this is a non-gamethread, the object is appended with ‘Async’ blocking it from deletion.
For now we removed the need for FWidget3DSceneProxy to use the Body Setup and also prevented non-gamethreads to create the bodysetup in UWidgetComponent::UpdateBodySetup and this seems to prevent the issue. Is there another solution you may have that can resolve the problem? Also surely the BodySetup is not needed if the collision settings are set to ‘No Collision’ right?
Hi William,
I haven’t had any luck reproducing this yet, would you be able to share the render thread callstack when the BodySetup is being created? In my testing (streaming a level with a widget component in and out), CreateSceneProxy is called on the game thread and the proxy constructor ends up returning the existing BodySetup since one was already created via BeginPlay. It sounds like there may be some scenario here where the proxy is created from the render thread prior to BeginPlay initializing the BodySetup, so I’d like to dig into how that can occur since that seems like it could be a bigger issue.
You’re probably correct that we don’t need a BodySetup if we aren’t going to support any collision (and that may be an optimization we could make), but it seems the default here is to be set up in such a way that we can support interactions with the content widget. Your workaround seems reasonable, but if there’s an existing bug where that BodySetup is created on the render thread and it causes problems then we’ll want to dig into that.
Best,
Cody
Hi,
Thanks for providing that, it looks like LevelStreaming.AsyncRegisterLevelContext would allow for the component to be created on a worker thread (which explains what you’re seeing here). We’ll want to support this threaded creation of the BodySetup since we may need it in some cases for hit detection on the widget, but I’ll pass this along to the proper team to weigh in on how to handle the GC issue since we’d expect that the component could still be properly cleaned up on world destruction.
Best,
Cody
Good day, before I investigate further can you tell us whether the CVar that Cody mentioned, LevelStreaming.AsyncRegisterLevelContext, is enabled in your project?
Good to know! When you repro the problem again with a debugger attached and see UWidgetComponent::UpdateBodySetup being called from the worker thread, can you inspect and share the callstack from the GameThread? I’d like to see which ParallelFor is kicking off the work resulting in that callstack.
I’m also curious why the BodySetup was not created sooner. I see that under normal circumenstances the BodySetup is created on RegisterComponent executed on the GameThread, so then it wouldn’t have the Async object flag. Depending on the GameThread callstack we’ll get closer to understanding whether this is an engine bug or not.
Hi Cody,
Sure, this is the callstack we found creating the BodySetup on the renderthread worker threads
> Project.self![Inline Function] UWidgetComponent::UpdateBodySetup::__unnamed_type_a4d3179b::operator()() Line 1938 + 0x2C bytes C++ Project.self!UWidgetComponent::UpdateBodySetup(bool bDrawSizeChanged) Line 1938 C++ Project.self!UWidgetComponent::GetBodySetup() Line 898 C++ Project.self![Inline Function] FWidget3DSceneProxy::FWidget3DSceneProxy(UWidgetComponent* InComponent, ISlate3DRenderer& InRenderer) Line 326 + 0x9 bytes C++ Project.self![Inline Function] FWidget3DSceneProxy::FWidget3DSceneProxy(UWidgetComponent* InComponent, ISlate3DRenderer& InRenderer) Line 327 C++ Project.self!UWidgetComponent::CreateSceneProxy() Line 802 C++ 0x00000010C24E8E00 C++ [Frames below may be incorrect and/or missing] Project.self!FActorPrimitiveComponentInterface::CreateSceneProxy() Line 5245 C++ Project.self!FScene::BatchAddPrimitivesInternal<UPrimitiveComponent>(TArrayView<UPrimitiveComponent*,int> InPrimitives) Line 1902 + 0x11 bytes C++ Project.self!FScene::AddPrimitive(UPrimitiveComponent* Primitive) Line 1803 + 0xA bytes C++ Project.self![Inline Function] FRegisterComponentContext::Process::__unnamed_type_748f4a87::operator()(int32 Index) Line 151 + 0xC bytes C++ Project.self![Inline Function] Invoke<(anon_class:0xAB7A7EC3ECC340D4)&,int>(FRegisterComponentContext::Process::__unnamed_type_748f4a87& Func, int&& Args) Line 47 C++ Project.self!UE::Core::Private::Function::TFunctionRefCaller<(anon_class:0xAB7A7EC3ECC340D4),void,int>::Call(void* Obj, int& Params) Line 315 C++ Project.self!ParallelForImpl::ParallelForInternal<TFunctionRef<void(int)>,(anon_class:0xF277747EE68159C4),decltype(nullptr)>::FParallelExecutor::operator()(const bool bIsMaster) Line 351 C++ Project.self!LowLevelTasks::TTaskDelegate<LowLevelTasks::FTask*(bool),48u>::TTaskDelegateImpl<(anon_class:0x636C2295001D02F9),false>::CallAndMove(LowLevelTasks::TTaskDelegate<LowLevelTasks::FTask*(bool),48u>::ThisClass& Destination, void* InlineData, uint32 DestInlineSize, bool Params) Line 172 C++ Project.self![Inline Function] LowLevelTasks::FTask::ExecuteTask() Line 630 C++ Project.self!LowLevelTasks::FScheduler::ExecuteTask(LowLevelTasks::FTask* InTask) Line 245 C++ Project.self![Inline Function] LowLevelTasks::FScheduler::TryExecuteTaskFrom<LowLevelTasks::Private::TLocalQueueRegistry<1024u,1024u>::TLocalQueue(illegal_value)(LowLevelTasks::Private::FWaitEvent* WaitEvent, LowLevelTasks::Private::TLocalQueueRegistry<1024u,1024u>::TLocalQueue* Queue, LowLevelTasks::Private::FOutOfWork& OutOfWork) Line 457 + 0x8 bytes C++ Project.self!LowLevelTasks::FScheduler::WorkerLoop(LowLevelTasks::Private::FWaitEvent* WorkerEvent, LowLevelTasks::FSchedulerTls::FLocalQueueType* WorkerLocalQueue, uint32 WaitCycles, bool bPermitBackgroundWork) Line 513 C++ Project.self!LowLevelTasks::FScheduler::WorkerMain(LowLevelTasks::Private::FWaitEvent* WorkerEvent, LowLevelTasks::FSchedulerTls::FLocalQueueType* WorkerLocalQueue, uint32 WaitCycles, bool bPermitBackgroundWork) Line 571 + 0xC bytes C++ Project.self!UE::Core::Private::Function::TFunctionRefCaller<(anon_class:0xD68B9870FAFC0A51),void>::Call(void* Obj) Line 321 C++ Project.self!FThreadImpl::Run() Line 68 C++ Project.self!FRunnableThreadPThread::Run() Line 27 C++ Project.self!FRunnableThreadPThread::_ThreadProc(void* pThis) Line 188 C++
For us we were loading and unloading the level containing the widget component actor multiple times, sometimes the level would not get the chance to fully load/unload before toggling the level’s load state. This seems to be the cause, almost seems like a race condition although in our case it caused a 100% repro
I don’t see anything related to that Cvar in the code base, nothing related to AsyncRegisterLevelContext exists
It all seems to be within FRegisterComponentContext::Process where there is a ParallelFor loop that has the call to AddPrimitive (which causes the BodySetup to be created), the ParallelFor loop will always be done on worker threads.