Improve thread safety in sound and physics systems
Refactored SoundChannel and SoundManager to use explicit locking for OpenAL operations and channel assignment, preventing race conditions during parallel sound playback. Added thread-local stacks in DynamicTree to ensure thread safety during parallel physics queries and raycasts. These changes address concurrency issues when sounds or physics queries are triggered from multiple threads.
This commit is contained in:
@@ -503,14 +503,24 @@ namespace Barotrauma.Sounds
|
||||
mutex = new object();
|
||||
}
|
||||
|
||||
// Use the playingChannels lock to protect both channel assignment AND OpenAL operations.
|
||||
// This prevents race conditions when multiple threads try to play sounds simultaneously
|
||||
// (e.g., during Parallel.ForEach in MapEntity.UpdateAll).
|
||||
int poolIndex = (int)sound.SourcePoolIndex;
|
||||
object channelsLock = sound.Owner.GetPlayingChannelsLock(sound.SourcePoolIndex);
|
||||
|
||||
#if !DEBUG
|
||||
try
|
||||
{
|
||||
#endif
|
||||
if (mutex != null) { Monitor.Enter(mutex); }
|
||||
if (sound.Owner.CountPlayingInstances(sound) < sound.MaxSimultaneousInstances)
|
||||
lock (channelsLock)
|
||||
{
|
||||
ALSourceIndex = sound.Owner.AssignFreeSourceToChannel(this);
|
||||
if (mutex != null) { Monitor.Enter(mutex); }
|
||||
try
|
||||
{
|
||||
if (sound.Owner.CountPlayingInstancesUnsafe(sound, poolIndex) < sound.MaxSimultaneousInstances)
|
||||
{
|
||||
ALSourceIndex = sound.Owner.AssignFreeSourceToChannelUnsafe(this, poolIndex);
|
||||
}
|
||||
|
||||
if (ALSourceIndex >= 0)
|
||||
@@ -585,18 +595,18 @@ namespace Barotrauma.Sounds
|
||||
SetProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (mutex != null) { Monitor.Exit(mutex); }
|
||||
}
|
||||
}
|
||||
#if !DEBUG
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
#endif
|
||||
if (mutex != null) { Monitor.Exit(mutex); }
|
||||
#if !DEBUG
|
||||
}
|
||||
#endif
|
||||
|
||||
void SetProperties()
|
||||
|
||||
@@ -417,6 +417,15 @@ namespace Barotrauma.Sounds
|
||||
return sourcePools[(int)poolIndex].ALSources[srcInd];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the lock object for the playing channels array for a specific pool.
|
||||
/// Used to protect OpenAL operations that need to be atomic with channel assignment.
|
||||
/// </summary>
|
||||
public object GetPlayingChannelsLock(SourcePoolIndex poolIndex)
|
||||
{
|
||||
return playingChannels[(int)poolIndex];
|
||||
}
|
||||
|
||||
public int AssignFreeSourceToChannel(SoundChannel newChannel)
|
||||
{
|
||||
if (Disabled) { return -1; }
|
||||
@@ -427,6 +436,18 @@ namespace Barotrauma.Sounds
|
||||
|
||||
lock (playingChannels[poolIndex])
|
||||
{
|
||||
return AssignFreeSourceToChannelUnsafe(newChannel, poolIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assigns a free source to a channel without locking.
|
||||
/// Caller MUST hold the playingChannels[poolIndex] lock before calling this method.
|
||||
/// </summary>
|
||||
public int AssignFreeSourceToChannelUnsafe(SoundChannel newChannel, int poolIndex)
|
||||
{
|
||||
if (Disabled) { return -1; }
|
||||
|
||||
for (int i = 0; i < playingChannels[poolIndex].Length; i++)
|
||||
{
|
||||
if (playingChannels[poolIndex][i] == null || !playingChannels[poolIndex][i].IsPlaying)
|
||||
@@ -436,7 +457,6 @@ namespace Barotrauma.Sounds
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//we couldn't get a free source to assign to this channel!
|
||||
return -1;
|
||||
@@ -476,13 +496,25 @@ namespace Barotrauma.Sounds
|
||||
int count = 0;
|
||||
lock (playingChannels[(int)sound.SourcePoolIndex])
|
||||
{
|
||||
for (int i = 0; i < playingChannels[(int)sound.SourcePoolIndex].Length; i++)
|
||||
{
|
||||
if (playingChannels[(int)sound.SourcePoolIndex][i] != null &&
|
||||
playingChannels[(int)sound.SourcePoolIndex][i].Sound.Filename == sound.Filename)
|
||||
{
|
||||
if (playingChannels[(int)sound.SourcePoolIndex][i].IsPlaying) { count++; };
|
||||
count = CountPlayingInstancesUnsafe(sound, (int)sound.SourcePoolIndex);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Counts playing instances without locking.
|
||||
/// Caller MUST hold the playingChannels[poolIndex] lock before calling this method.
|
||||
/// </summary>
|
||||
public int CountPlayingInstancesUnsafe(Sound sound, int poolIndex)
|
||||
{
|
||||
if (Disabled) { return 0; }
|
||||
int count = 0;
|
||||
for (int i = 0; i < playingChannels[poolIndex].Length; i++)
|
||||
{
|
||||
if (playingChannels[poolIndex][i] != null &&
|
||||
playingChannels[poolIndex][i].Sound.Filename == sound.Filename)
|
||||
{
|
||||
if (playingChannels[poolIndex][i].IsPlaying) { count++; };
|
||||
}
|
||||
}
|
||||
return count;
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using FarseerPhysics.Common;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using Microsoft.Xna.Framework;
|
||||
@@ -74,8 +75,15 @@ namespace FarseerPhysics.Collision
|
||||
/// </summary>
|
||||
public class DynamicTree<T>
|
||||
{
|
||||
private Stack<int> _raycastStack = new Stack<int>(256);
|
||||
private Stack<int> _queryStack = new Stack<int>(256);
|
||||
// Thread-local stacks to ensure thread safety during parallel queries/raycasts
|
||||
[ThreadStatic]
|
||||
private static Stack<int> _raycastStack;
|
||||
[ThreadStatic]
|
||||
private static Stack<int> _queryStack;
|
||||
|
||||
private static Stack<int> RaycastStack => _raycastStack ??= new Stack<int>(256);
|
||||
private static Stack<int> QueryStack => _queryStack ??= new Stack<int>(256);
|
||||
|
||||
private int _freeList;
|
||||
private int _nodeCapacity;
|
||||
private int _nodeCount;
|
||||
@@ -346,12 +354,12 @@ namespace FarseerPhysics.Collision
|
||||
/// <param name="aabb">The aabb.</param>
|
||||
public void Query(Func<int, bool> callback, ref AABB aabb, ref Body body)
|
||||
{
|
||||
_queryStack.Clear();
|
||||
_queryStack.Push(_root);
|
||||
QueryStack.Clear();
|
||||
QueryStack.Push(_root);
|
||||
|
||||
while (_queryStack.Count > 0)
|
||||
while (QueryStack.Count > 0)
|
||||
{
|
||||
int nodeId = _queryStack.Pop();
|
||||
int nodeId = QueryStack.Pop();
|
||||
if (nodeId == NullNode)
|
||||
{
|
||||
continue;
|
||||
@@ -386,8 +394,8 @@ namespace FarseerPhysics.Collision
|
||||
}
|
||||
else
|
||||
{
|
||||
_queryStack.Push(node.Child1);
|
||||
_queryStack.Push(node.Child2);
|
||||
QueryStack.Push(node.Child1);
|
||||
QueryStack.Push(node.Child2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -395,12 +403,12 @@ namespace FarseerPhysics.Collision
|
||||
|
||||
public void Query(Func<int, bool> callback, ref AABB aabb)
|
||||
{
|
||||
_queryStack.Clear();
|
||||
_queryStack.Push(_root);
|
||||
QueryStack.Clear();
|
||||
QueryStack.Push(_root);
|
||||
|
||||
while (_queryStack.Count > 0)
|
||||
while (QueryStack.Count > 0)
|
||||
{
|
||||
int nodeId = _queryStack.Pop();
|
||||
int nodeId = QueryStack.Pop();
|
||||
if (nodeId == NullNode)
|
||||
{
|
||||
continue;
|
||||
@@ -419,8 +427,8 @@ namespace FarseerPhysics.Collision
|
||||
}
|
||||
else
|
||||
{
|
||||
_queryStack.Push(_nodes[nodeId].Child1);
|
||||
_queryStack.Push(_nodes[nodeId].Child2);
|
||||
QueryStack.Push(_nodes[nodeId].Child1);
|
||||
QueryStack.Push(_nodes[nodeId].Child2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -460,12 +468,12 @@ namespace FarseerPhysics.Collision
|
||||
Vector2.Max(ref p1, ref t, out segmentAABB.UpperBound);
|
||||
}
|
||||
|
||||
_raycastStack.Clear();
|
||||
_raycastStack.Push(_root);
|
||||
RaycastStack.Clear();
|
||||
RaycastStack.Push(_root);
|
||||
|
||||
while (_raycastStack.Count > 0)
|
||||
while (RaycastStack.Count > 0)
|
||||
{
|
||||
int nodeId = _raycastStack.Pop();
|
||||
int nodeId = RaycastStack.Pop();
|
||||
if (nodeId == NullNode)
|
||||
{
|
||||
continue;
|
||||
@@ -522,8 +530,8 @@ namespace FarseerPhysics.Collision
|
||||
}
|
||||
else
|
||||
{
|
||||
_raycastStack.Push(_nodes[nodeId].Child1);
|
||||
_raycastStack.Push(_nodes[nodeId].Child2);
|
||||
RaycastStack.Push(_nodes[nodeId].Child1);
|
||||
RaycastStack.Push(_nodes[nodeId].Child2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user