diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs
index df9d68998..ade827da5 100644
--- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs
+++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundChannel.cs
@@ -503,86 +503,102 @@ 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 (ALSourceIndex >= 0)
- {
- if (!IsStream)
+ if (mutex != null) { Monitor.Enter(mutex); }
+ try
{
- Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, 0);
- int alError = Al.GetError();
- if (alError != Al.NoError)
+ if (sound.Owner.CountPlayingInstancesUnsafe(sound, poolIndex) < sound.MaxSimultaneousInstances)
{
- throw new Exception("Failed to reset source buffer: " + debugName + ", " + Al.GetErrorString(alError));
+ ALSourceIndex = sound.Owner.AssignFreeSourceToChannelUnsafe(this, poolIndex);
}
- Sound.FillAlBuffers();
- if (Sound.Buffers is not { AlBuffer: not 0, AlMuffledBuffer: not 0 }) { return; }
-
- uint alBuffer = sound.Owner.GetCategoryMuffle(category) || muffled ? Sound.Buffers.AlMuffledBuffer : Sound.Buffers.AlBuffer;
- Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, (int)alBuffer);
- alError = Al.GetError();
- if (alError != Al.NoError)
+ if (ALSourceIndex >= 0)
{
- throw new Exception("Failed to bind buffer to source (" + ALSourceIndex.ToString() + ":" + sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex) + "," + alBuffer.ToString() + "): " + debugName + ", " + Al.GetErrorString(alError));
- }
+ if (!IsStream)
+ {
+ Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, 0);
+ int alError = Al.GetError();
+ if (alError != Al.NoError)
+ {
+ throw new Exception("Failed to reset source buffer: " + debugName + ", " + Al.GetErrorString(alError));
+ }
- SetProperties();
+ Sound.FillAlBuffers();
+ if (Sound.Buffers is not { AlBuffer: not 0, AlMuffledBuffer: not 0 }) { return; }
- Al.SourcePlay(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex));
- alError = Al.GetError();
- if (alError != Al.NoError)
- {
- throw new Exception("Failed to play source: " + debugName + ", " + Al.GetErrorString(alError));
+ uint alBuffer = sound.Owner.GetCategoryMuffle(category) || muffled ? Sound.Buffers.AlMuffledBuffer : Sound.Buffers.AlBuffer;
+ Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, (int)alBuffer);
+ alError = Al.GetError();
+ if (alError != Al.NoError)
+ {
+ throw new Exception("Failed to bind buffer to source (" + ALSourceIndex.ToString() + ":" + sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex) + "," + alBuffer.ToString() + "): " + debugName + ", " + Al.GetErrorString(alError));
+ }
+
+ SetProperties();
+
+ Al.SourcePlay(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex));
+ alError = Al.GetError();
+ if (alError != Al.NoError)
+ {
+ throw new Exception("Failed to play source: " + debugName + ", " + Al.GetErrorString(alError));
+ }
+ }
+ else
+ {
+ uint alBuffer = 0;
+ Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, (int)alBuffer);
+ int alError = Al.GetError();
+ if (alError != Al.NoError)
+ {
+ throw new Exception("Failed to reset source buffer: " + debugName + ", " + Al.GetErrorString(alError));
+ }
+
+ Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Looping, Al.False);
+ alError = Al.GetError();
+ if (alError != Al.NoError)
+ {
+ throw new Exception("Failed to set stream looping state: " + debugName + ", " + Al.GetErrorString(alError));
+ }
+
+ streamShortBuffer = new short[STREAM_BUFFER_SIZE];
+
+ streamBuffers = new uint[4];
+ unqueuedBuffers = new uint[4];
+ streamBufferAmplitudes = new float[4];
+ for (int i = 0; i < 4; i++)
+ {
+ Al.GenBuffer(out streamBuffers[i]);
+
+ alError = Al.GetError();
+ if (alError != Al.NoError)
+ {
+ throw new Exception("Failed to generate stream buffers: " + debugName + ", " + Al.GetErrorString(alError));
+ }
+
+ if (!Al.IsBuffer(streamBuffers[i]))
+ {
+ throw new Exception("Generated streamBuffer[" + i.ToString() + "] is invalid! " + debugName);
+ }
+ }
+ Sound.Owner.InitUpdateChannelThread();
+ SetProperties();
+ }
}
}
- else
+ finally
{
- uint alBuffer = 0;
- Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Buffer, (int)alBuffer);
- int alError = Al.GetError();
- if (alError != Al.NoError)
- {
- throw new Exception("Failed to reset source buffer: " + debugName + ", " + Al.GetErrorString(alError));
- }
-
- Al.Sourcei(sound.Owner.GetSourceFromIndex(Sound.SourcePoolIndex, ALSourceIndex), Al.Looping, Al.False);
- alError = Al.GetError();
- if (alError != Al.NoError)
- {
- throw new Exception("Failed to set stream looping state: " + debugName + ", " + Al.GetErrorString(alError));
- }
-
- streamShortBuffer = new short[STREAM_BUFFER_SIZE];
-
- streamBuffers = new uint[4];
- unqueuedBuffers = new uint[4];
- streamBufferAmplitudes = new float[4];
- for (int i = 0; i < 4; i++)
- {
- Al.GenBuffer(out streamBuffers[i]);
-
- alError = Al.GetError();
- if (alError != Al.NoError)
- {
- throw new Exception("Failed to generate stream buffers: " + debugName + ", " + Al.GetErrorString(alError));
- }
-
- if (!Al.IsBuffer(streamBuffers[i]))
- {
- throw new Exception("Generated streamBuffer[" + i.ToString() + "] is invalid! " + debugName);
- }
- }
- Sound.Owner.InitUpdateChannelThread();
- SetProperties();
+ if (mutex != null) { Monitor.Exit(mutex); }
}
}
#if !DEBUG
@@ -591,12 +607,6 @@ namespace Barotrauma.Sounds
{
throw;
}
- finally
- {
-#endif
- if (mutex != null) { Monitor.Exit(mutex); }
-#if !DEBUG
- }
#endif
void SetProperties()
diff --git a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs
index 0826aa2d3..e320bf3d9 100644
--- a/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs
+++ b/Barotrauma/BarotraumaClient/ClientSource/Sounds/SoundManager.cs
@@ -417,6 +417,15 @@ namespace Barotrauma.Sounds
return sourcePools[(int)poolIndex].ALSources[srcInd];
}
+ ///
+ /// 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.
+ ///
+ public object GetPlayingChannelsLock(SourcePoolIndex poolIndex)
+ {
+ return playingChannels[(int)poolIndex];
+ }
+
public int AssignFreeSourceToChannel(SoundChannel newChannel)
{
if (Disabled) { return -1; }
@@ -427,14 +436,25 @@ namespace Barotrauma.Sounds
lock (playingChannels[poolIndex])
{
- for (int i = 0; i < playingChannels[poolIndex].Length; i++)
+ return AssignFreeSourceToChannelUnsafe(newChannel, poolIndex);
+ }
+ }
+
+ ///
+ /// Assigns a free source to a channel without locking.
+ /// Caller MUST hold the playingChannels[poolIndex] lock before calling this method.
+ ///
+ 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)
{
- if (playingChannels[poolIndex][i] == null || !playingChannels[poolIndex][i].IsPlaying)
- {
- if (playingChannels[poolIndex][i] != null) { playingChannels[poolIndex][i].Dispose(); }
- playingChannels[poolIndex][i] = newChannel;
- return i;
- }
+ if (playingChannels[poolIndex][i] != null) { playingChannels[poolIndex][i].Dispose(); }
+ playingChannels[poolIndex][i] = newChannel;
+ return i;
}
}
@@ -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++)
+ count = CountPlayingInstancesUnsafe(sound, (int)sound.SourcePoolIndex);
+ }
+ return count;
+ }
+
+ ///
+ /// Counts playing instances without locking.
+ /// Caller MUST hold the playingChannels[poolIndex] lock before calling this method.
+ ///
+ 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[(int)sound.SourcePoolIndex][i] != null &&
- playingChannels[(int)sound.SourcePoolIndex][i].Sound.Filename == sound.Filename)
- {
- if (playingChannels[(int)sound.SourcePoolIndex][i].IsPlaying) { count++; };
- }
+ if (playingChannels[poolIndex][i].IsPlaying) { count++; };
}
}
return count;
diff --git a/Libraries/Farseer Physics Engine 3.5/Collision/DynamicTree.cs b/Libraries/Farseer Physics Engine 3.5/Collision/DynamicTree.cs
index fac9982bd..6bc690f09 100644
--- a/Libraries/Farseer Physics Engine 3.5/Collision/DynamicTree.cs
+++ b/Libraries/Farseer Physics Engine 3.5/Collision/DynamicTree.cs
@@ -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
///
public class DynamicTree
{
- private Stack _raycastStack = new Stack(256);
- private Stack _queryStack = new Stack(256);
+ // Thread-local stacks to ensure thread safety during parallel queries/raycasts
+ [ThreadStatic]
+ private static Stack _raycastStack;
+ [ThreadStatic]
+ private static Stack _queryStack;
+
+ private static Stack RaycastStack => _raycastStack ??= new Stack(256);
+ private static Stack QueryStack => _queryStack ??= new Stack(256);
+
private int _freeList;
private int _nodeCapacity;
private int _nodeCount;
@@ -346,12 +354,12 @@ namespace FarseerPhysics.Collision
/// The aabb.
public void Query(Func 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 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);
}
}
}