Mutable Crash when marking as garbage an UCustomizableObjectInstance

Hey, hello everyone!.

We are using Mutable to create weapons that can contain a variety of attachments. The idea is to build them at runtime based on the different attachments.

In the player base we have a kind of workbench where you can preview your weapons (the 3D model). When the player selects a weapon, we generate the mesh and display it on top of the table. When the player selects another weapon, we destroy the previous

UCustomizableObjectInstance with

MarkAsGarbage and then create a new one for the new weapon.The problem is that if you switch weapons quickly, the previous instance may be marked as garbage before the

UpdateMesh task finishes, which leads to a crash due to a weak pointer in the provided callstack.On the other hand, if I don’t mark it as garbage and wait for all references to finish — even if I force garbage collection every frame — everything completes without crashing. This makes me think that somewhere inside Mutable, the system is still holding a reference to that

UCustomizableObjectInstance, preventing deletion and avoiding the crash.

CleanupCustomizableObjectInstance()
{
    UCustomizableObjectInstance* CustomizableObjectInstance = GetCustomizableObjectInstance();
    if (IsValid(CustomizableObjectInstance))
    {
       CustomizableObjectInstance->UpdatedNativeDelegate.Clear();
       CustomizableObjectInstance->MarkAsGarbage();
    }
    SetCustomizableObjectInstance(nullptr);
}

Question:

What is the proper way of cleaning up a

UCustomizableObjectInstance safely, without running into these issues?

[Inlined] IsObjectHandleTypeSafe(FObjectHandlePrivate) ObjectHandle.h:187
[Inlined] ObjectPtr_Private::Get(const FObjectPtr &) ObjectPtr.h:469
[Inlined] TObjectPtr::Get() ObjectPtr.h:681
[Inlined] TObjectPtr::operator class UCustomizableObject *() ObjectPtr.h:708
UCustomizableObjectInstance::GetCustomizableObject() CustomizableObjectInstance.cpp:682
impl::Task_Mutable_GetMeshes_GetImages(const TSharedRef<…> &, double) CustomizableObjectSystem.cpp:2537
impl::Task_Mutable_GetMeshes_GetMesh_Loop(const TSharedRef<…> &, double, const TSharedRef<…> &, int) CustomizableObjectSystem.cpp:2770
impl::Task_MutableGetMeshes_GetMesh_Post(const TSharedRef<…> &, double, const TSharedRef<…> &, int, TTask<…>) CustomizableObjectSystem.cpp:2755
[Inlined] impl::Task_Mutable_GetMeshes_GetMesh_Loop::__l2::<lambda_1>::operator()() CustomizableObjectSystem.cpp:2780
[Inlined] Invoke(<lambda_1> &) Invoke.h:47
UE::Tasks::Private::TExecutableTaskBase<`impl::Task_Mutable_GetMeshes_GetMesh_Loop'::`2'::<lambda_1>,void,void>::ExecuteTask() TaskPrivate.h:904
UE::Tasks::Private::FTaskBase::TryExecuteTask() TaskPrivate.h:527
[Inlined] UE::Tasks::Private::FTaskBase::Init::__l2::<lambda_1>::operator()() TaskPrivate.h:184
[Inlined] LowLevelTasks::FTask::Init::__l13::<lambda_1>::operator()(const bool) Task.h:499
[Inlined] Invoke(LowLevelTasks::FTask::<lambda_1> &, bool &) Invoke.h:47
[Inlined] LowLevelTasks::TTaskDelegate<LowLevelTasks::FTask * __cdecl(bool),48>::TTaskDelegateImpl<`LowLevelTasks::FTask::Init<`UE::Tasks::Private::FTaskBase::Init'::`2'::<lambda_1> >'::`13'::<lambda_1>,0>::Call(void *,bool) TaskDelegate.h:162
LowLevelTasks::TTaskDelegate<LowLevelTasks::FTask * __cdecl(bool),48>::TTaskDelegateImpl<`LowLevelTasks::FTask::Init<`UE::Tasks::Private::FTaskBase::Init'::`2'::<lambda_1> >'::`13'::<lambda_1>,0>::CallAndMove(LowLevelTasks::TTaskDelegate<LowLevelTasks::FTask * __cdecl(bool),48> &,void *,unsigned int,bool) TaskDelegate.h:171
[Inlined] LowLevelTasks::TTaskDelegate::CallAndMove(LowLevelTasks::TTaskDelegate<…> &, bool) TaskDelegate.h:309
LowLevelTasks::FTask::ExecuteTask() Task.h:627
LowLevelTasks::FScheduler::ExecuteTask(LowLevelTasks::FTask *) Scheduler.cpp:387
[Inlined] LowLevelTasks::FScheduler::TryExecuteTaskFrom(LowLevelTasks::Private::FWaitEvent *, LowLevelTasks::Private::TLocalQueueRegistry<…>::TLocalQueue *, LowLevelTasks::Private::FOutOfWork &, bool) Scheduler.cpp:665
LowLevelTasks::FScheduler::WorkerLoop(LowLevelTasks::Private::FWaitEvent *, LowLevelTasks::Private::TLocalQueueRegistry<…>::TLocalQueue *, unsigned int, bool) Scheduler.cpp:724
[Inlined] LowLevelTasks::FScheduler::WorkerMain(LowLevelTasks::Private::FWaitEvent *, LowLevelTasks::Private::TLocalQueueRegistry<…>::TLocalQueue *, unsigned int, bool) Scheduler.cpp:783
`LowLevelTasks::FScheduler::CreateWorker'::`2'::<lambda_1>::operator()() Scheduler.cpp:188
[Inlined] UE::Core::Private::Function::TFunctionRefBase::operator()() Function.h:471
FThreadImpl::Run() Thread.cpp:66
FRunnableThreadWin::Run() WindowsRunnableThread.cpp:156
FRunnableThreadWin::GuardedRun() WindowsRunnableThread.cpp:71

Hi,

thanks for reaching out. The Mutable system should be optimised to handle deletion of runtime objects and you should generally not need to worry about having to explicitly mark them as garbage.

To check the validity of a non-reflected raw C++ pointer, it’s recommended to use IsValidLowLevel(), i.e. if(CustomizableObjectInstance->IsValidLowLevel()) instead of IsValid(CustomizableObjectInstance), as the pointer might be stale (please see the section Testing Pointers in this article).

Hopefully that helps. Please let me know if you have any further questions or comments.

Best,

Sam

Hey, thanks for the answer :slight_smile:

All right, that’s fine. I don’t need to force the destroy, so I can just let the GC eventually handle it.

On the other hand, I want to point out that marking the CustomizableObject as garbage isn’t possible right now, because if you do it while Mutable is generating the mesh, the weak pointer stored inside the context returns null and it crashes. This might be a bug, or maybe MarkAsGarbage is simply not meant to be used.

The code that is causing the crash is:

CustomizableObjectSystem.cpp::2536

/** Gather all GetImages that have to be called. */
	void Task_Mutable_GetMeshes_GetImages(
		const TSharedRef<FUpdateContextPrivate>& OperationData,
		double StartTime)
	{
		MUTABLE_CPUPROFILER_SCOPE(Task_Mutable_GetMeshes_GetImages)
		
		const UCustomizableObjectInstance* Instance = OperationData->Instance.Get();
		const UCustomizableObject* CustomizableObject = Instance->GetCustomizableObject();
		const UModelResources& ModelResources = *CustomizableObject->GetPrivate()->GetModelResources();
 
		const mu::FInstance* MutableInstance = OperationData->MutableInstance.Get();

[Image Removed]Which is the weak pointer that return null because the object is marked for garbage, then the next line crashes

[Image Removed]

>> On the other hand, I want to point out that marking the CustomizableObject as garbage isn’t possible right now, because if you do it while Mutable is generating the mesh, the weak pointer stored inside the context returns null and it crashes. This might be a bug, or maybe MarkAsGarbage is simply not meant to be used.

Thanks for pointing that out. Forcing garbage collection with MarkAsGarbage is generally something you want to avoid doing, as that is what the GC system is for. To be sure, would it be possible to provide the crash callstack (or even better a simple project to reproduce this crash)? That way I can report it as an issue to Epic.

Thanks,

Sam