Component Visualizer Hit Proxy Crash - Passed UActorComponent eaten by GC

Hiya!

I’m making a custom component interface for branching structures and basing the implementation on SplineComponentVisualizer. Generally things work as expected but I’m stumped by this bug - occasionally I get a crash when interacting with the Primitive Drawing Interface via Hit Proxies.
I can’t reproduce it consistently - sometimes it happens almost immediately, sometimes I have to click around alot.


I can see that the UActorComponent pointer references my object but it is marked as TRASH and tagged with RF_MirroredGarbage. This then results in a null pointer further down the call stack.
I’ve tried activating garbage collection every frame (gc.CollectGarbageEveryFrame 1) with no effect.
The component is attached to a clean Actor blueprint & I’ve replicated this with another blueprint.

Here’s the full call stack:

Unhandled Exception: EXCEPTION_ACCESS_VIOLATION reading address 0x0000000000000008

UnrealEditor_Engine!UEngineElementsLibrary::CreateComponentElement() [D:\build\++UE5\Sync\Engine\Source\Runtime\Engine\Private\Elements\Framework\EngineElementsLibrary.cpp:440]
UnrealEditor_Engine!UE::Core::Private::Function::TFunctionRefCaller<TTypedElementOwner<FComponentElementData> (__cdecl*)(UActorComponent const * __ptr64),TTypedElementOwner<FComponentElementData> __cdecl(UActorComponent const * __ptr64)>::Call() [D:\build\++UE5\Sync\Engine\Source\Runtime\Core\Public\Templates\Function.h:396]
UnrealEditor_Engine!UE::Core::Private::Function::TFunctionRefCaller<`EngineElementsLibraryUtil::AcquireEditorTypedElementHandle<UActorComponent,FComponentElementData>'::`5'::<lambda_1>,TTypedElementOwner<FComponentElementData> __cdecl(void)>::Call() [D:\build\++UE5\Sync\Engine\Source\Runtime\Core\Public\Templates\Function.h:396]
UnrealEditor_Engine!TTypedElementOwnerStore<FComponentElementData,UActorComponent const * __ptr64>::FindOrRegisterElementOwner() [D:\build\++UE5\Sync\Engine\Source\Runtime\TypedElementFramework\Public\Elements\Framework\TypedElementOwnerStore.h:177]
UnrealEditor_Engine!EngineElementsLibraryUtil::AcquireEditorTypedElementHandle<UActorComponent,FComponentElementData>() [D:\build\++UE5\Sync\Engine\Source\Runtime\Engine\Private\Elements\Framework\EngineElementsLibrary.cpp:66]
UnrealEditor_Engine!UEngineElementsLibrary::AcquireEditorComponentElementHandle() [D:\build\++UE5\Sync\Engine\Source\Runtime\Engine\Private\Elements\Framework\EngineElementsLibrary.cpp:470]
UnrealEditor_Graph3DEditor_Win64_DebugGame!HComponentVisProxy::GetElementHandle() [C:\Program Files\Epic Games\UE_5.4\Engine\Source\Editor\UnrealEd\Public\ComponentVisualizer.h:56]
UnrealEditor_UnrealEd!FLevelEditorViewportClient::ProcessClick() [D:\build\++UE5\Sync\Engine\Source\Editor\UnrealEd\Private\LevelEditorViewport.cpp:2820]
UnrealEditor_UnrealEd!FEditorViewportClient::ProcessClickInViewport() [D:\build\++UE5\Sync\Engine\Source\Editor\UnrealEd\Private\EditorViewportClient.cpp:3343]
UnrealEditor_UnrealEd!FEditorViewportClient::Internal_InputKey() [D:\build\++UE5\Sync\Engine\Source\Editor\UnrealEd\Private\EditorViewportClient.cpp:3125]
UnrealEditor_UnrealEd!FLevelEditorViewportClient::InputKey() [D:\build\++UE5\Sync\Engine\Source\Editor\UnrealEd\Private\LevelEditorViewport.cpp:3545]
UnrealEditor_Engine!FSceneViewport::OnMouseButtonUp() [D:\build\++UE5\Sync\Engine\Source\Runtime\Engine\Private\Slate\SceneViewport.cpp:651]
UnrealEditor_Slate!SViewport::OnMouseButtonUp() [D:\build\++UE5\Sync\Engine\Source\Runtime\Slate\Private\Widgets\SViewport.cpp:247]
UnrealEditor_Slate!TArray<TSharedRef<SWindow,1>,TSizedDefaultAllocator<32> >::RemoveAll<`TArray<TSharedRef<SWindow,1>,TSizedDefaultAllocator<32> >::Remove'::`2'::<lambda_1> >() [D:\build\++UE5\Sync\Engine\Source\Runtime\Slate\Private\Framework\Application\SlateApplication.cpp:442]
UnrealEditor_Slate!FSlateApplication::RoutePointerUpEvent() [D:\build\++UE5\Sync\Engine\Source\Runtime\Slate\Private\Framework\Application\SlateApplication.cpp:5279]
UnrealEditor_Slate!FSlateApplication::ProcessMouseButtonUpEvent() [D:\build\++UE5\Sync\Engine\Source\Runtime\Slate\Private\Framework\Application\SlateApplication.cpp:5857]
UnrealEditor_Slate!FSlateApplication::OnMouseUp() [D:\build\++UE5\Sync\Engine\Source\Runtime\Slate\Private\Framework\Application\SlateApplication.cpp:5813]
UnrealEditor_ApplicationCore!FWindowsApplication::ProcessDeferredMessage() [D:\build\++UE5\Sync\Engine\Source\Runtime\ApplicationCore\Private\Windows\WindowsApplication.cpp:2243]
UnrealEditor_ApplicationCore!FWindowsApplication::DeferMessage() [D:\build\++UE5\Sync\Engine\Source\Runtime\ApplicationCore\Private\Windows\WindowsApplication.cpp:2750]
UnrealEditor_ApplicationCore!FWindowsApplication::ProcessMessage() [D:\build\++UE5\Sync\Engine\Source\Runtime\ApplicationCore\Private\Windows\WindowsApplication.cpp:1919]
UnrealEditor_ApplicationCore!FWindowsApplication::AppWndProc() [D:\build\++UE5\Sync\Engine\Source\Runtime\ApplicationCore\Private\Windows\WindowsApplication.cpp:929]
user32
user32
UnrealEditor_ApplicationCore!FWindowsPlatformApplicationMisc::PumpMessages() [D:\build\++UE5\Sync\Engine\Source\Runtime\ApplicationCore\Private\Windows\WindowsPlatformApplicationMisc.cpp:145]
UnrealEditor_Win64_DebugGame!FEngineLoop::Tick() [D:\build\++UE5\Sync\Engine\Source\Runtime\Launch\Private\LaunchEngineLoop.cpp:5850]
UnrealEditor_Win64_DebugGame!GuardedMain() [D:\build\++UE5\Sync\Engine\Source\Runtime\Launch\Private\Launch.cpp:180]
UnrealEditor_Win64_DebugGame!GuardedMainWrapper() [D:\build\++UE5\Sync\Engine\Source\Runtime\Launch\Private\Windows\LaunchWindows.cpp:118]
UnrealEditor_Win64_DebugGame!LaunchWindowsStartup() [D:\build\++UE5\Sync\Engine\Source\Runtime\Launch\Private\Windows\LaunchWindows.cpp:258]
UnrealEditor_Win64_DebugGame!WinMain() [D:\build\++UE5\Sync\Engine\Source\Runtime\Launch\Private\Windows\LaunchWindows.cpp:298]
UnrealEditor_Win64_DebugGame!__scrt_common_main_seh() [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288]
kernel32
ntdll

Here are the functions for handling clicks and creating the hit proxies:

bool FGraph3DVisualizer::VisProxyHandleClick(FEditorViewportClient* InViewportClient, HComponentVisProxy* VisProxy, const FViewportClick& Click)
{
	if (VisProxy && VisProxy->Component.IsValid())
	{
		SelectionState->Modify();
		SelectionState->SetComponentPropertyPath(FComponentPropertyPath((UGraph3DComponent*)VisProxy->Component.Get()));

		if (!DeselectedInEditorDelegateHandle.IsValid())
			{
				DeselectedInEditorDelegateHandle = GetEditedGraphComponent()->OnDeselectedInEditor.AddRaw(this, &FGraph3DVisualizer::OnDeselectedInEditor);
		}

		if (Click.GetKey() == EKeys::RightMouseButton) return true;
		
		const HGraph3DNodeVisProxy* NodeProxy = (HGraph3DNodeVisProxy*)VisProxy;
		if (NodeProxy->IsA(HGraph3DNodeVisProxy::StaticGetType()) && NodeProxy)
		{
			SelectionType = Node;
			SelectedEdges.Empty();

			if (InViewportClient->IsCtrlPressed()) {
				SelectedNodes.AddUnique(NodeProxy->NodeIndex);
			} else {
				SelectedNodes.Empty();
				SelectedNodes.AddUnique(NodeProxy->NodeIndex);
			}

			GEditor->RedrawLevelEditingViewports(true);
			return true;
		}
		
		const HGraph3DEdgeVisProxy* EdgeProxy = (HGraph3DEdgeVisProxy*)VisProxy;
		if (EdgeProxy->IsA(HGraph3DEdgeVisProxy::StaticGetType()) && EdgeProxy)
		{
			SelectionType = Edge;
			SelectedNodes.Empty();
			
			if (InViewportClient->IsCtrlPressed()) {
				SelectedEdges.AddUnique(EdgeProxy->EdgeIndex);
			} else {
				SelectedEdges.Empty();
				SelectedEdges.AddUnique(EdgeProxy->EdgeIndex);
			}

			
			
			GEditor->RedrawLevelEditingViewports(true);
			return true;
		}
	}

	SelectionState->SetComponentPropertyPath(FComponentPropertyPath());

	return false;
}
void FGraph3DVisualizer::DrawVisualization(const UActorComponent* Component, const FSceneView* View, FPrimitiveDrawInterface* PDI)
{
	const UGraph3DComponent* Graph = Cast<const UGraph3DComponent>(Component);
	if (!Graph) return;
	if (!Component->IsValidLowLevel()) {
		UE_LOGFMT(LogTemp, Log, "Invalid Graph Component");
		return;
	}

	//draw edges
	for(int32 i = 0; i < Graph->Edges.Num(); i++)
	{
		PDI->SetHitProxy(NULL);
		PDI->SetHitProxy(new HGraph3DEdgeVisProxy(Component, i));
		const FVector Start = Graph->Nodes[Graph->Edges[i].Start].Position + Graph->GetComponentLocation();
		const FVector End = Graph->Nodes[Graph->Edges[i].End].Position + Graph->GetComponentLocation();
		//PDI->DrawLine(Start, End, FColor::Emerald, SDPG_Foreground, 3,0,true);

		FLinearColor Color = FColor::MakeRandomSeededColor(i);
		FVector Direction = End - Start;
		const float Offset = Graph->NodeScale;
		float Length = Direction.Length() - (Offset * 2);
		float Width = 12.f;
		float Thickness = 2.5f;
		if (SelectedEdges.Contains(i)) Thickness *= 2;
		Direction = Direction.GetUnsafeNormal();
		FMatrix Matrix = FScaleRotationTranslationMatrix(FVector::One(), Direction.Rotation(), Start + (Direction * Offset));
		
		DrawDirectionalArrow(PDI, Matrix, Color, Length, Width, SDPG_Foreground, Thickness);
		PDI->SetHitProxy(NULL);

	}

	//draw nodes
	for (int32 i = 0; i < Graph->Nodes.Num(); i++)
	{
		PDI->SetHitProxy(NULL);
		PDI->SetHitProxy(new HGraph3DNodeVisProxy(Component, i));
		FVector Position = Graph->Nodes[i].Position + Graph->GetComponentLocation();
		float Scale = Graph->NodeScale;
		FColor Color = FColor::White;
		if (SelectedNodes.Contains(i))
		{
			Scale *= 1.5;
			Color = FColor::Yellow;
		}
		PDI->DrawPoint(Position, Color, Scale, SDPG_Foreground);
		PDI->SetHitProxy(NULL);
	}

}

I would really appreciate any insight into how my component is being garbage collected or what I could be doing wrong. Thanks! :pray:

P.S. This is my first time using C++ so pre-emtive apologies for my ignorance :alien:

1 Like

there is the useful article to GC Memory Management & Garbage Collection in Unreal Engine 5 | Cas Mikelis' Game Blog

in short, you should protect all your objects from GC, you could choose how - with UPROPERTY, add to root, override gc mechanics in the UObject directly

2 Likes

That’s a good article, thanks!

There are some types of objects that have a mechanism to register with their owners, and automatically create a hard reference to them. For example, actor components. Actors also have a similar mechanism - after spawning them, they will remain referenced by the level they are in.

In this case, I’m confused why an actor component would be garbage collected - it should be protected as it’s referenced by the actor (which isn’t marked for garbage collection, just the component).

UPROPERTY(hard references) vs TWeakObjectPtr and Garbage Collection - Programming & Scripting / C++ - Epic Developer Community Forums (unrealengine.com)

There IS come caveats with ActorComponents though. The DestroyActor function will actually mark all components the Actor has as PendingKill. BUT components can be GC’d if they are not held by a strong pointer that is UPROPERTY as AActor does NOT hold components in a UPROPERTY container.

However this implies I need to register the ActorComponent.
I’ll try some things out and check out some examples of components, I could have missed something in SplineComponent or its visualizer which registers it.

1 Like

I think I have figured this out!
Digging a bit more into the SplineComponentVisualizer, there are calls to this function:
GEditor->RedrawLevelEditingViewports(true);
in anything which edits the component structure. The passed bool is bInvalidateHitProxies , which implies that I need to ensure hit proxies are invalidated after each action when editing the component.
This was a good learning experience :+1:

1 Like