Bug: Game Hitches When Opening Options Due to Audio Device Enumeration
150 °C
Bug #16477

Game Hitches When Opening Options Due to Audio Device Enumeration

Status: Open
Priority:
High
Added by:
math.h
on September 4, '26
Assigned to:
Unassigned
Due date:
Reported for:

Steps to Reproduce

  1. Press esc
  2. Press options
  3. lag

Explanation

Options_OutputAudioSource_C.FillAudioDevices
calls GetAvaliableAudioOutputDevices for each Array_Get(index) in whatever foreach loop you are doing. which is full WASAPI device enumeration, returning entire list.
it hitches the game for 4 seconds for me every time i open Options, and probably for other people too.
please don't do that every again <3 your biggest game breaker and cheater.

signed @math.h

Reconstructed functions from disassembly:

==================================================
bool UFSDGameUserSettings::GetAvaliableAudioOutputDevices(UObject* WorldContextObject,
TArray<FString>& OutAudioDevices)
{
// WorldContextObject is accepted but never read.
// NOTE: never cleared. append only

FAudioDevice* Device = GEngine->GetMainAudioDeviceRaw();
if (!Device)
return false;

if (!Device->bAudioMixerModuleLoaded)
return false;

IAudioMixerPlatformInterface* Platform =
static_cast<Audio::FMixerDevice*>(Device)->AudioMixerPlatform;
if (!Platform)
return false;

uint32 NumDevices = 0;
if (!Platform->GetNumOutputDevices(NumDevices))
return false;

for (uint32 Index = 0; Index < NumDevices; ++Index)
{
// Default ctor calls Reset(), which heap-allocates two "Unknown" FStrings and resets the
// channel array - all freed again at the bottom of the loop. Three allocations per device.
Audio::FAudioPlatformDeviceInfo Info;

if (Platform->GetOutputDeviceInfo(Index, Info)) // the slow call
{
OutAudioDevices.Add(Info.DeviceId);
}
}

// The entire success condition. Nothing else contributes.
return OutAudioDevices.Num() > 0;
}

==================================================

FString UFSDGameUserSettings::GetAudioOutputDeviceName(UObject* WorldContextObject,
FString DeviceID)
{
// DeviceID is by value, not const&: the epilogue frees its buffer on both exit paths, which is
// the MSVC ABI for a non-trivial by-value parameter (passed by pointer, destroyed by callee).
// This is a LINEAR SCAN - no cache, no map. O(N) platform queries per call.

FAudioDevice* Device = GEngine->GetMainAudioDeviceRaw();
if (!Device)
return FString();

if (!Device->bAudioMixerModuleLoaded)
return FString();

IAudioMixerPlatformInterface* Platform =
static_cast<Audio::FMixerDevice*>(Device)->AudioMixerPlatform;
if (!Platform)
return FString();

uint32 NumDevices = 0;
if (!Platform->GetNumOutputDevices(NumDevices))
return FString();

for (uint32 Index = 0; Index < NumDevices; ++Index)
{
Audio::FAudioPlatformDeviceInfo Info; // 3 allocations, per device

if (Platform->GetOutputDeviceInfo(Index, Info)) // the slow call
{
//Comparator is inlined in binary
if (Info.DeviceId.Equals(DeviceID, ESearchCase::CaseSensitive))
return Info.Name;
}
}

return FString(); // exhausted the device list without a match
}

==================================================

From the first one we can see that checking array length is literally the same as checking output bool

From the second we can see multiple slow calls per EVERY call into it, calling and checking again and again, which is fine once, but becomes not fine REALLY FAST when you have a lot of output devices.

Both functions are declared BlueprintPure. Both function are not converted to impure calls, so every out wire in the editor is +1 call to them

Each issues one WASAPI device-property query per device, with three heap allocations per device, and neither caches.

A pure node has no exec pin, so the Kismet compiler inlines a fresh evaluation at every read of every pin it feeds. FillAudioDevices wires GetAvaliableAudioOutputDevices straight into a ForEachLoop's Array pin - which the macro reads several times per iteration.

This also includes the length check of the macro.

Suggested fixes:

  1. Cache enumeration and invalidate on audio device change notification (this removes O(N^2) term)
  2. Give GetAudioOutputDeviceName a map lookup, or pass an index, so you only have one lookup, and you just valid check the index at entry
  3. in FillAudioDevices promote the first call to the GetAvaliableAudioOutputDevices to a local variable, and feed that. OR as ue5 allows, convert pure function into impure call.
  4. Drop BlueprintPure from both (why do we have such expensive calls being treated the same as math functions)

Watchers

Video Clips

None

Log Files

Log files are not visible to you

This project restricts log file visibility.
Device

Device information is not visible to you

This project restricts device information visibility.
No comments yet.
Loading comments...
Loading comments...
0 comments loaded

You need to join this project to comment on issues. Join Project