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!