Bug: Game Hitches When Opening Options Due to Audio Device Enumeration
Bug #16477
Game Hitches When Opening Options Due to Audio Device Enumeration
| Status: | Open |
| Priority: |
High |
| Added by: |
|
| Assigned to: |
Unassigned
|
| Due date: | |
| Reported for: |
RC:EA:02.00.01
Auto
|
Steps to Reproduce
- Press esc
- Press options
- 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:
- Cache enumeration and invalidate on audio device change notification (this removes O(N^2) term)
- Give GetAudioOutputDeviceName a map lookup, or pass an index, so you only have one lookup, and you just valid check the index at entry
- in
FillAudioDevicespromote the first call to theGetAvaliableAudioOutputDevicesto a local variable, and feed that. OR as ue5 allows, convert pure function into impure call. - Drop BlueprintPure from both (why do we have such expensive calls being treated the same as math functions)
Watchers
Screenshots
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.