Просмотр исходного кода

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.
Ryan C. Gordon 1 день назад
Родитель
Сommit
6136358840
1 измененных файлов с 31 добавлено и 11 удалено
  1. 31 11
      src/audio/SDL_audio.c

+ 31 - 11
src/audio/SDL_audio.c

@@ -739,6 +739,21 @@ SDL_AudioDevice *SDL_AddAudioDevice(bool recording, const char *name, const SDL_
     return device;
     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.
 // 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)
 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));
     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);
     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.
     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 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.
         // on non-default devices, dump everything.
@@ -822,12 +829,15 @@ static void SDLCALL SDL_AudioDeviceDisconnected_OnMainThread(void *userdata)
 
 
 void SDL_AudioDeviceDisconnected(SDL_AudioDevice *device)
 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
     // lots of risk of various audio backends deadlocking because they're calling
     // this while holding a backend-specific lock, which causes problems when we
     // 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
     // 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.
     // that lock to be released. So just queue the work on the main thread.
     if (device) {
     if (device) {
         RefPhysicalAudioDevice(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);
         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.
         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;
     bool failed = false;
     int buffer_size = device->buffer_size;
     int buffer_size = device->buffer_size;
     Uint8 *device_buffer = device->GetDeviceBuf(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.
         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;
     bool failed = false;
 
 
     if (!device->logical_devices) {
     if (!device->logical_devices) {