1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-08-13 19:54:17 +02:00

Fixed possible multithreading races in sound/music callbacks

This commit is contained in:
Ivan Savenko
2023-08-08 00:27:03 +03:00
parent 1eb58bcc32
commit ed066cb7ba
2 changed files with 60 additions and 42 deletions

View File

@@ -14,6 +14,7 @@
#include "CMusicHandler.h" #include "CMusicHandler.h"
#include "CGameInfo.h" #include "CGameInfo.h"
#include "renderSDL/SDLRWwrapper.h" #include "renderSDL/SDLRWwrapper.h"
#include "gui/CGuiHandler.h"
#include "../lib/JsonNode.h" #include "../lib/JsonNode.h"
#include "../lib/GameConstants.h" #include "../lib/GameConstants.h"
@@ -185,9 +186,9 @@ int CSoundHandler::playSound(std::string sound, int repeats, bool cache)
Mix_FreeChunk(chunk); Mix_FreeChunk(chunk);
} }
else if (cache) else if (cache)
callbacks[channel]; initCallback(channel);
else else
callbacks[channel] = [chunk](){ Mix_FreeChunk(chunk);}; initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);});
} }
else else
channel = -1; channel = -1;
@@ -237,28 +238,50 @@ void CSoundHandler::setChannelVolume(int channel, ui32 percent)
void CSoundHandler::setCallback(int channel, std::function<void()> function) void CSoundHandler::setCallback(int channel, std::function<void()> function)
{ {
std::map<int, std::function<void()> >::iterator iter; boost::unique_lock lockGuard(mutexCallbacks);
iter = callbacks.find(channel);
auto iter = callbacks.find(channel);
//channel not found. It may have finished so fire callback now //channel not found. It may have finished so fire callback now
if(iter == callbacks.end()) if(iter == callbacks.end())
function(); function();
else else
iter->second = function; iter->second.push_back(function);
} }
void CSoundHandler::soundFinishedCallback(int channel) void CSoundHandler::soundFinishedCallback(int channel)
{ {
std::map<int, std::function<void()> >::iterator iter; boost::unique_lock lockGuard(mutexCallbacks);
iter = callbacks.find(channel);
if (iter == callbacks.end()) if (callbacks.count(channel) == 0)
return; return;
auto callback = std::move(iter->second); // store callbacks from container locally - SDL might reuse this channel for another sound
callbacks.erase(iter); // but do actualy execution in separate thread, to avoid potential deadlocks in case if callback requires locks of its own
auto callback = callbacks.at(channel);
callbacks.erase(channel);
if (callback) if (!callback.empty())
callback(); {
GH.dispatchMainThread([callback](){
for (auto entry : callback)
entry();
});
}
}
void CSoundHandler::initCallback(int channel)
{
boost::unique_lock lockGuard(mutexCallbacks);
assert(callbacks.count(channel) == 0);
callbacks[channel] = {};
}
void CSoundHandler::initCallback(int channel, const std::function<void()> & function)
{
boost::unique_lock lockGuard(mutexCallbacks);
assert(callbacks.count(channel) == 0);
callbacks[channel].push_back(function);
} }
int CSoundHandler::ambientGetRange() const int CSoundHandler::ambientGetRange() const
@@ -471,44 +494,32 @@ void CMusicHandler::setVolume(ui32 percent)
void CMusicHandler::musicFinishedCallback() void CMusicHandler::musicFinishedCallback()
{ {
// boost::mutex::scoped_lock guard(mutex); // call music restart in separate thread to avoid deadlock in some cases
// FIXME: WORKAROUND FOR A POTENTIAL DEADLOCK
// It is possible for: // It is possible for:
// 1) SDL thread to call this method on end of playback // 1) SDL thread to call this method on end of playback
// 2) VCMI code to call queueNext() method to queue new file // 2) VCMI code to call queueNext() method to queue new file
// this leads to: // this leads to:
// 1) SDL thread waiting to acquire music lock in this method (while keeping internal SDL mutex locked) // 1) SDL thread waiting to acquire music lock in this method (while keeping internal SDL mutex locked)
// 2) VCMI thread waiting to acquire internal SDL mutex (while keeping music mutex locked) // 2) VCMI thread waiting to acquire internal SDL mutex (while keeping music mutex locked)
// Because of that (and lack of clear way to fix that)
// We will try to acquire lock here and if failed - do nothing
// This may break music playback till next song is enqued but won't deadlock the game
if (!mutex.try_lock()) GH.dispatchMainThread([this]()
{ {
logGlobal->error("Failed to acquire mutex! Unable to restart music!"); boost::unique_lock lockGuard(mutex);
return; if (current.get() != nullptr)
}
if (current.get() != nullptr)
{
// if music is looped, play it again
if (current->play())
{ {
mutex.unlock(); // if music is looped, play it again
return; if (current->play())
return;
else
current.reset();
} }
else
{
current.reset();
}
}
if (current.get() == nullptr && next.get() != nullptr) if (current.get() == nullptr && next.get() != nullptr)
{ {
current.reset(next.release()); current.reset(next.release());
current->play(); current->play();
} }
mutex.unlock(); });
} }
MusicEntry::MusicEntry(CMusicHandler *owner, std::string setName, std::string musicURI, bool looped, bool fromStart): MusicEntry::MusicEntry(CMusicHandler *owner, std::string setName, std::string musicURI, bool looped, bool fromStart):

View File

@@ -45,9 +45,13 @@ private:
Mix_Chunk *GetSoundChunk(std::string &sound, bool cache); Mix_Chunk *GetSoundChunk(std::string &sound, bool cache);
//have entry for every currently active channel /// have entry for every currently active channel
//std::function will be nullptr if callback was not set /// vector will be empty if callback was not set
std::map<int, std::function<void()> > callbacks; std::map<int, std::vector<std::function<void()>> > callbacks;
/// Protects access to callbacks member to avoid data races:
/// SDL calls sound finished callbacks from audio thread
boost::mutex mutexCallbacks;
int ambientDistToVolume(int distance) const; int ambientDistToVolume(int distance) const;
void ambientStopSound(std::string soundId); void ambientStopSound(std::string soundId);
@@ -58,6 +62,9 @@ private:
std::map<std::string, int> ambientChannels; std::map<std::string, int> ambientChannels;
std::map<int, int> channelVolumes; std::map<int, int> channelVolumes;
void initCallback(int channel, const std::function<void()> & function);
void initCallback(int channel);
public: public:
CSoundHandler(); CSoundHandler();