FVoiceSerializeHelper doesn't add references to AudioComponent instances

In 4.7.0, looking at the implementation of FVoiceEngineImpl, it appears that FVoiceSerializerHelper is responsible for keeping the RemoteTalkerBuffer AudioComponents from being garbage collected. However, I’ve set breakpoints (in Debug build) in FVoiceSerializeHelper::AddReferencedObjects and never get any hits. Eventually the garbage collector notices and when it disposes UAudioComponent instances, FVoiceEngineImpl::SubmitRemoteVoiceData ends up crashing due to stale pointers to UAudioComponent.

How are the UAudioComponents created by FVoiceEngineImpl supposed to be protected from the garbage collector?

Thanks - brad

This implementation is working better for us:

class FVoiceSerializeHelper : public FGCObject
{
	/** Reference to audio components */
	FVoiceEngineImpl* VoiceEngine;
	FVoiceSerializeHelper() :
		VoiceEngine(NULL)
	{}

	UPROPERTY()
	TArray ReferencedObjects;
public:

	FVoiceSerializeHelper(FVoiceEngineImpl* InVoiceEngine) :
		VoiceEngine(InVoiceEngine)
	{}
	~FVoiceSerializeHelper()
	{
		for (auto ReferencedObject : ReferencedObjects)
		{
			RemoveObject(ReferencedObject);
		}		
	}
	
	void AddObject(UObject* Object)
	{
		ReferencedObjects.AddUnique(Object);
		Object->AddToRoot();		
	}

	void RemoveObject(UObject* Object)
	{
		Object->RemoveFromRoot();
		ReferencedObjects.Remove(Object);		
	}

	virtual void AddReferencedObjects(FReferenceCollector& Collector) override
	{
		Collector.AddReferencedObjects(ReferencedObjects);
	}
};

It is paired with direct calls to AddObject and RemoveObject when AudioComponents are created and orphaned by FVoiceEngineImpl.

I have seen one exit crash in RemoveObject due to a stale Object pointer. I haven’t tracked that down yet but at least this one survives a GC collect.

Still interested to see if there’s a better implementation/fix.

That seems like odd and unexpected behavior. I just ran a test and saw that when the GC runs, it does in fact call into the FGCObject::AddReferencedObject code for voice.

That code has been around for over a year and I have not experienced any crashes related to it. In fact, now it should be even more obvious because I create one component per remote voice and only Stop the component after a brief period of inactivity rather than throw away / create new.

The helper is instantiated the first time any remote voice packet reaches the SubmitRemoteVoiceData function. The code works in a self registration fashion in the constructor, can you verify that it is in fact being added to the list of GC aware registrants? The AddReferencedObject function should be called independent of the existence of any UAudioComponent objects to save.

I believe I’ve solved the problem, new answer to be upvoted. Short answer is it has been fixed since 4.7

Larger explanation.

Look in the function FArchiveRealtimeGC::PerformReachabilityAnalysis

there are two ways this special UObject gets added to the ObjectsToSerialize array.

The common way is that it is a RF_RootSet object and therefore has special code for handling that

if( Object->HasAnyFlags( RF_RootSet ) )

But the problem lay with the iterator that controls this loop

for ( FRawObjectIterator It(true); It; ++It )

This iterator only operates on UObjects greater than the “is disregard for GC” count which is the total of all UObjects that exist at the end of engine init. This is an optimization that says “these objects will never GC so never consider them”. But if the GCObjectReferencer is under that index/number then it won’t get added via this code path.

So just above the iterator is some new code, code I believe you are missing.

	// Make sure GC referencer object is checked for references to other objects even if it resides in permanent object pool
	if (FPlatformProperties::RequiresCookedData() && FGCObject::GGCObjectReferencer && GetUObjectArray().IsDisregardForGC(FGCObject::GGCObjectReferencer))
	{
		ObjectsToSerialize.Add(FGCObject::GGCObjectReferencer);
	}

Which is making sure this object is considered if it is below that number. DefaultEngine.ini for ShooterGame sets the number to 50000, so the iterator is skipping this very important object.

[Core.System]
MaxObjectsNotConsideredByGC=50000

CL#2446988 would be your fix.

So sorry this took so long. Fortnite has that MaxObjectsNotConsideredByGC value set to 0, so this new code was never needed.

I hope this solves your problem and we can all go home now.

Hi,

We think this post contains useful information which we would like to share with our public UE4 community. With your approval, we would like to make a copy of this post on the public AnswerHub which includes the discussion but strips out your username and company name. Please let us know if you are okay with this.

Thanks!

Hi Brad,

We’ll respond as soon as possible. Thanks for your patience.

Sean

To make sure I didn’t have any local changes causing the GC problem I went through the following steps to verify this is a problem with latest code and no modifications except the ones listed below:

  1. Downloaded clean copy of 4.7.5 from github into c:\ue4

  2. ran setup.exe to pick up additional dependencies

  3. generateprojectfiles.exe and launched c:\ue4\ue4.sln in vs2013

  4. built Development Editor config and launched the editor

  5. downloaded the 4.7.0 shooter game from marketplace and generated c:\ue4\shootergame

  6. made the following modifications:

    Engine\Config\BaseEngine.ini
    [Voice]
    bEnabled=true
    Engine\Config\DefaultEngine.ini
    [OnlineSubsystem]
    DefaultPlatformService=Null
    PollingIntervalInMs=20
    bHasVoiceEnabled=true
    ShooterGame\Config\DefaultGame.ini
    [/Script/Engine.GameSession]
    bRequiresPushToTalk=false

  7. packaged a DebugGame, looking at output:

BuildCookRun -nocompileeditor -nop4 -project=C:/ue4/ShooterGame/ShooterGame.uproject -cook -allmaps -stage -archive -archivedirectory=C:/ue4/Archive -package -clientconfig=DebugGame -ue4exe=UE4Editor-Cmd.exe -clean -pak -prereqs -nodebuginfo -targetplatform=Win64 -build -utf8output

  1. launched C:\ue4\Archive\WindowsNoEditor\ShooterGame\Binaries\Win64\ShooterGame-Win64-DebugGame.exe and hosted game on machine1: no bots, free for all, HighRise map
  2. launched C:\ue4\Archive\WindowsNoEditor\ShooterGame\Binaries\Win64\ShooterGame-Win64-DebugGame.exe and joined game on machine2
  3. on machine2 made sure to voice data is captured and replicated to machine1
    one machine1 open console with ~ and type “obj gc”. The game will immediately crash while processing packets:
>	ShooterGame-Win64-DebugGame.exe!UObjectBaseUtility::IsA(const UClass * SomeBase) Line 254	C++
 	ShooterGame-Win64-DebugGame.exe!CastChecked(USoundBase * Src, ECastCheckedType::Type CheckType) Line 151	C++
 	ShooterGame-Win64-DebugGame.exe!FVoiceEngineImpl::SubmitRemoteVoiceData(const FUniqueNetId & RemoteTalkerId, unsigned char * Data, unsigned int * Size) Line 385	C++
 	ShooterGame-Win64-DebugGame.exe!FOnlineVoiceImpl::ProcessRemoteVoicePackets() Line 741	C++
 	ShooterGame-Win64-DebugGame.exe!FOnlineVoiceImpl::Tick(float DeltaTime) Line 106	C++
 	ShooterGame-Win64-DebugGame.exe!FOnlineSubsystemNull::Tick(float DeltaTime) Line 133	C++
 	ShooterGame-Win64-DebugGame.exe!FTicker::Tick(float DeltaTime) Line 90	C++
 	ShooterGame-Win64-DebugGame.exe!FEngineLoop::Tick() Line 2352	C++
 	ShooterGame-Win64-DebugGame.exe!GuardedMain(const wchar_t * CmdLine, HINSTANCE__ * hInInstance, HINSTANCE__ * hPrevInstance, int nCmdShow) Line 142	C++
 	ShooterGame-Win64-DebugGame.exe!WinMain(HINSTANCE__ * hInInstance, HINSTANCE__ * hPrevInstance, char * __formal, int nCmdShow) Line 191	C++
 	ShooterGame-Win64-DebugGame.exe!__tmainCRTStartup() Line 618	C
 	kernel32.dll!00007ffbe69913d2()	Unknown
 	ntdll.dll!00007ffbe903e954()	Unknown

Adding to root makes the need to use AddReferencedObjects obsolete. Anything in the root set will be default never be garbage collected. If it is calling AddReferencedObjects in your example, it should be calling AddReferencedObjects in the original code because the foundation is the same.

Your Add/Remove logic just makes it impossible for the object to be GC’d, which I agree is the point, but should have been unnecessary.

Great thanks for the repro, I’ll see if I can get QA on this to repro locally and I’ll investigate from there. I appreciate it.

Josh - Was QA able to repro?

Hi Brad,

Sorry for the delay. We were able to reproduce the crash on our end in a new ShooterGame project using the steps you provided. Josh is looking into it further from here. Thanks for the detailed repro!

So I’m not seeing this behavior in non packaged builds, I’m going to cook/run and see why there might be a difference.

The GC helper object is working as intended so far. Weird that cooked/packaged would make a difference.

I ran a packaged build at my desk via the editor at latest code (4.8+) and we have QA running a more recent build also. I don’t see this happening at all.

It all comes down to

SerializeHelper = new FVoiceSerializeHelper(this);

getting called and then internally

FGCObject::Init

properly registering the class with GGCObjectReferencer

Can you tell me which part of this flow doesn’t work? I assume the memory gets initialized? And Init is called? Does the array inside GGCObjectReferencer not grow when you add the this pointer? It does have an AddUnique call, but I would be really worried if our TArray function was failing.

I just regenerated my test case from 4.7.6 using the instructions QA used to repro and it still seems to be a problem. (I had to delete the original to free up some disk space.)

Internally the FGCObject constructor calls FGCObject::StaticInit in the 4.7.6 code… not sure if ::Init is a 4.8 difference or not.

So the issue isn’t that the SerializeHelper object isn’t being properly added to the static GGCObjectReferencer. It appears to be that the SerializeHelper’s AddReferencedObjects never gets called so the VoiceEngine->RemoteTalkerBuffer AudioComponents aren’t being added to the Collector and thus aren’t protected from garbage collection.

To confirm that FVoiceEngineImpl::FVoiceSerializeHelper::AddReferencedObjects wasn’t being called I tried moving it into VoiceEngineImpl.cpp and set a breakpoint on machine1 (the server / client1) in my test instructions. (Sometimes the debugger isn’t perfect when setting breakpoints in header files.) The breakpoint never got hit. Then just to be sure I added a “check(false);” in the function and that also had no effect.

I also ran these same tests in a pure Debug build and got the same results.

Thanks Josh! That was the fix. I tested just applying the code above to GarbageCollector.cpp in 4.7.6 and it fixed the problem.

Go for it and thanks again for your help on this one.