SDL_mixer: api: Allow MIX_DestroyTrack from a mixer callback without crashing. (fc197)

From fc1971d2193238a06a2aafb005b9dfc22179e206 Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Thu, 16 Apr 2026 14:20:13 -0400
Subject: [PATCH] api: Allow MIX_DestroyTrack from a mixer callback without
 crashing.

(cherry picked from commit a2d867c1830e2a48089dbfefc1a5fdb6e8c80a7a)
---
 include/SDL3_mixer/SDL_mixer.h |  5 ++++-
 src/SDL_mixer.c                | 18 ++++++++++++++++++
 src/SDL_mixer_internal.h       |  2 ++
 3 files changed, 24 insertions(+), 1 deletion(-)

diff --git a/include/SDL3_mixer/SDL_mixer.h b/include/SDL3_mixer/SDL_mixer.h
index b6d6ee01..d6f896fe 100644
--- a/include/SDL3_mixer/SDL_mixer.h
+++ b/include/SDL3_mixer/SDL_mixer.h
@@ -1115,7 +1115,10 @@ extern SDL_DECLSPEC MIX_Track * SDLCALL MIX_CreateTrack(MIX_Mixer *mixer);
  * MIX_SetTrackStoppedCallback(), it will _not_ be called.
  *
  * If the mixer is currently mixing in another thread, this will block until
- * it finishes.
+ * it finishes. Destroying a track from the mixer thread itself (during a
+ * callback) will cause it to be destroyed as soon as this iteration of the
+ * mixer thread is not using it; in this scenario, destroying a track and then
+ * making futher changes to it is considered undefined behavior.
  *
  * Destroying a NULL MIX_Track is a legal no-op.
  *
diff --git a/src/SDL_mixer.c b/src/SDL_mixer.c
index e9b16ef7..242d6412 100644
--- a/src/SDL_mixer.c
+++ b/src/SDL_mixer.c
@@ -575,6 +575,9 @@ static void SDLCALL MixerCallback(void *userdata, SDL_AudioStream *stream, int a
         MIX_Track *next_track = NULL;
         for (MIX_Track *track = group->tracks; track; track = next_track) {
             next_track = track->group_next;  // this won't save you from a callback going totally rogue, but it'll deal with the current track leaving the group.
+
+            track->currently_mixing = true;
+
             const int to_be_read = (additional_amount / SDL_AUDIO_FRAMESIZE(mixer->spec)) * SDL_AUDIO_FRAMESIZE(track->output_spec);
             const int br = SDL_GetAudioStreamData(track->output_stream, getbuf, to_be_read);
             if (br > 0) {
@@ -606,6 +609,11 @@ static void SDLCALL MixerCallback(void *userdata, SDL_AudioStream *stream, int a
                         break;
                 }
             }
+
+            track->currently_mixing = false;
+            if (track->destroy_requested) {  // callback asked to destroy the track while we were still using it.
+                MIX_DestroyTrack(track);  // actually kill it now.
+            }
         }
 
         if (group_bytes > mixer->actual_mixed_bytes) {
@@ -1503,6 +1511,16 @@ void MIX_DestroyTrack(MIX_Track *track)
     MIX_Mixer *mixer = track->mixer;
 
     LockMixer(mixer);
+
+    // handle the case where someone destroys a track during a mixer callback.  :O
+    //  tracks are not currently reference-counted like MIX_Audio objects are, but
+    //  we'll catch this specific case for now.
+    if (track->currently_mixing) {
+        track->destroy_requested = true;
+        UnlockMixer(mixer);
+        return;
+    }
+
     if (track->prev) {
         track->prev->next = track->next;
     } else {
diff --git a/src/SDL_mixer_internal.h b/src/SDL_mixer_internal.h
index 906b47f4..0ed73cd1 100644
--- a/src/SDL_mixer_internal.h
+++ b/src/SDL_mixer_internal.h
@@ -148,6 +148,8 @@ struct MIX_Track
     MIX_Audio *input_audio;    // non-NULL if used with MIX_SetTrackAudioStream. Holds a reference.
     SDL_IOStream *io;  // used for MIX_SetTrackAudio and MIX_SetTrackIOStream. Might be owned by us (SDL_IOFromConstMem of MIX_Audio::precache), or owned by the app.
     MIX_IoClamp ioclamp;  // used for MIX_SetTrackAudio and MIX_SetTrackIOStream.
+    bool currently_mixing;  // true when the mixer is running on this track.
+    bool destroy_requested;  // true if MIX_DestroyTrack called while the track is actively mixing.
     bool closeio;  // true if we should close `io` when changing track data.
     bool halt_when_exhausted;  // true if we should stop the track when input runs out.
     SDL_AudioStream *input_stream;  // used for both MIX_SetTrackAudio and MIX_SetTrackAudioStream. Maybe not owned by SDL_mixer!