From 61363588407195518c5fe0f3c99a3175f0fb85ed Mon Sep 17 00:00:00 2001 From: "Ryan C. Gordon" Date: Tue, 9 Jun 2026 20:16:01 -0400 Subject: [PATCH] audio: Split device "zombie" status into multiple stages. Otherwise, a device that is disconnected in the standard audio device thread might keep failing WaitDevice() in a tight loop, each one generating a new main thread callback. In normal situations, this is wasteful, but if the app isn't pumping the event loop quickly (or at all!), this will quickly eat up all the memory in a machine. Now we note that the device is zombified right away, and device thread iteration will use this to replace the implementation with the Zombie equivalents once it owns the device lock. The main thread callback will progress to device->zombie==2, which it uses to decide if this is a duplicate disconnect notification. Since it also owns the lock at this point, it takes the moment to set the Zombie implementation up, too. This allows things (like the WASAPI backend) to check for a non-zero zombie state immediately without having to worry if the main thread callback ran, and for the standard audio threads to also move to the Zombie implementation without waiting on that callback. (The Zombie implementation is used to make a dead device keep processing, so things that need the audio device to make progress to function will keep working, and things blindly pushing to an audio stream won't queue up endless data that isn't being consumed.) Fixes #15745. --- src/audio/SDL_audio.c | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/src/audio/SDL_audio.c b/src/audio/SDL_audio.c index d642016541..eb5f6456e2 100644 --- a/src/audio/SDL_audio.c +++ b/src/audio/SDL_audio.c @@ -739,6 +739,21 @@ SDL_AudioDevice *SDL_AddAudioDevice(bool recording, const char *name, const SDL_ return device; } +// you must hold the device lock when calling this! +static void SetAudioDeviceZombieFunctions(SDL_AudioDevice *device) +{ + // Swap in "Zombie" versions of the usual platform interfaces, so the device will keep + // making progress until the app closes it. Otherwise, streams might continue to + // accumulate waste data that never drains, apps that depend on audio callbacks to + // progress will freeze, etc. + device->WaitDevice = ZombieWaitDevice; + device->GetDeviceBuf = ZombieGetDeviceBuf; + device->PlayDevice = ZombiePlayDevice; + device->WaitRecordingDevice = ZombieWaitDevice; + device->RecordDevice = ZombieRecordDevice; + device->FlushRecording = ZombieFlushRecording; +} + // Called when a device is removed from the system, or it fails unexpectedly, from any thread, possibly even the audio device's thread. static void SDLCALL SDL_AudioDeviceDisconnected_OnMainThread(void *userdata) { @@ -760,18 +775,10 @@ static void SDLCALL SDL_AudioDeviceDisconnected_OnMainThread(void *userdata) const bool is_default_device = ((devid == current_audio.default_playback_device_id) || (devid == current_audio.default_recording_device_id)); SDL_UnlockRWLock(current_audio.subsystem_rwlock); - const bool first_disconnect = SDL_CompareAndSwapAtomicInt(&device->zombie, 0, 1); + // zombie==2 means "we've handled the disconnect events". 1=="we marked this as dead from a random thread but haven't done anything else" 0==we think we're still alive. + const bool first_disconnect = SDL_CompareAndSwapAtomicInt(&device->zombie, 0, 2) || SDL_CompareAndSwapAtomicInt(&device->zombie, 1, 2); if (first_disconnect) { // if already disconnected this device, don't do it twice. - // Swap in "Zombie" versions of the usual platform interfaces, so the device will keep - // making progress until the app closes it. Otherwise, streams might continue to - // accumulate waste data that never drains, apps that depend on audio callbacks to - // progress will freeze, etc. - device->WaitDevice = ZombieWaitDevice; - device->GetDeviceBuf = ZombieGetDeviceBuf; - device->PlayDevice = ZombiePlayDevice; - device->WaitRecordingDevice = ZombieWaitDevice; - device->RecordDevice = ZombieRecordDevice; - device->FlushRecording = ZombieFlushRecording; + SetAudioDeviceZombieFunctions(device); // in case we beat the device thread to this. // on default devices, dump any logical devices that explicitly opened this device. Things that opened the system default can stay. // on non-default devices, dump everything. @@ -822,12 +829,15 @@ static void SDLCALL SDL_AudioDeviceDisconnected_OnMainThread(void *userdata) void SDL_AudioDeviceDisconnected(SDL_AudioDevice *device) { + //SDL_Log("AUDIO DEVICE DISCONNECTED %p '%s'", device, device ? device->name : NULL); + // lots of risk of various audio backends deadlocking because they're calling // this while holding a backend-specific lock, which causes problems when we // want to obtain the device lock while its audio thread is also waiting for // that lock to be released. So just queue the work on the main thread. if (device) { RefPhysicalAudioDevice(device); + SDL_CompareAndSwapAtomicInt(&device->zombie, 0, 1); // note that we're (un)dead right now, if we haven't already, but leave the event notifications for the main thread. SDL_RunOnMainThread(SDL_AudioDeviceDisconnected_OnMainThread, device, false); } } @@ -1183,6 +1193,11 @@ bool SDL_PlaybackAudioThreadIterate(SDL_AudioDevice *device) return false; // we're done, shut it down. } + if (SDL_GetAtomicInt(&device->zombie) == 1) { + // we've been marked as (un)dead but not fully processed. Set up the zombie functions so we stop talking to the real backend. + SetAudioDeviceZombieFunctions(device); + } + bool failed = false; int buffer_size = device->buffer_size; Uint8 *device_buffer = device->GetDeviceBuf(device, &buffer_size); @@ -1349,6 +1364,11 @@ bool SDL_RecordingAudioThreadIterate(SDL_AudioDevice *device) return false; // we're done, shut it down. } + if (SDL_GetAtomicInt(&device->zombie) == 1) { + // we've been marked as (un)dead but not fully processed. Set up the zombie functions so we stop talking to the real backend. + SetAudioDeviceZombieFunctions(device); + } + bool failed = false; if (!device->logical_devices) {