USynthSound::OnBeginGenerate ensure fires due to audio thread / game thread race on OwningSynthComponent

Hey Epic Friends!

So, USynthSound::OnBeginGenerate is called on the audio mixer thread. It currently guards OwningSynthComponent with ensure(OwningSynthComponent.IsValid()). When a USynthComponent is destroyed on the game thread between the AudioComponent->Play() call and the audio thread processing InitSourceOnBeginGenerate, OwningSynthComponent is already marked PendingKill and the ensure fires, boom!

Stack (audio mixer thread):

USynthSound::OnBeginGenerate (SynthComponent.cpp:39)  <-- ENSURE
Audio::FMixerSourceBuffer::OnBeginGenerate
Audio::FMixerSourceManager::InitSource  [audio thread command queue]
Audio::FMixerSourceManager::PumpCommandQueue
Audio::FMixerDevice::OnProcessAudioStream
Audio::IAudioMixerPlatformInterface::RunInternal

Root cause:

OnBeginGenerate, OnGeneratePCMAudio, and OnEndGenerate all run on the audio thread and all access OwningSynthComponent (a TWeakObjectPtr). The latter two already handle the race silently:

// OnGeneratePCMAudio (line 55) -- correct:
// Mark pending kill can null this out on the game thread in rare cases.
if (!OwningSynthComponent.IsValid())
    return 0;

// OnEndGenerate (line 67) -- correct:
if (OwningSynthComponent.IsValid())
    OwningSynthComponent->OnEndGenerate();

// OnBeginGenerate (line 39) -- inconsistent:
if (ensure(OwningSynthComponent.IsValid()))  // <-- fires under the same race
    OwningSynthComponent->OnBeginGenerate();

So we’re thinking about this little fix:

void USynthSound::OnBeginGenerate()
{
    // OwningSynthComponent can be marked PendingKill on the game thread in rare cases.
    if (USynthComponent* Comp = OwningSynthComponent.Get())
    {
        Comp->OnBeginGenerate();
    }
}

This matches the pattern already used in OnGeneratePCMAudio and OnEndGenerate. It’s doesn’t technically seem perfect though, because in a worst case scenario, the main thread might destroy the OwningSynthComponent right after the if check? But due to the way the current GC works, that seems very very very unlikely?

So some questions-

Is there a stronger fix planned (e.g. ensuring the audio source is fully stopped before the USynthComponent reaches BeginDestroy, eliminating the race entirely)?

Is the GC-freeing-of-memory window also considered safe given BeginDestroyStop() ordering, or is there a scenario where the audio thread can hold a raw Comp pointer past object destruction?

thanks!!

Josh

[Attachment Removed]

Hi Josh,

To be honest, we’ve largely moved our rendering to utilize the ISoundGenerator path instead of the USynthComponent.

ISoundGenerator allows you to have an instance of a rendering source on the audio render thread without needing to worry about GC since it is divorced from the UObject that created it. See USoundBase::CreateSoundGenerator for reference.

If you’re interested in code samples, MetaSounds is an example of a system which utilizes the ISoundGenerator, so you should be able to find examples there.

[Attachment Removed]