diff --git a/docs/docs/01_quickstart/00-project-setup.md b/docs/docs/01_quickstart/00-project-setup.md index 3afc47d6..96fa8cf9 100644 --- a/docs/docs/01_quickstart/00-project-setup.md +++ b/docs/docs/01_quickstart/00-project-setup.md @@ -27,7 +27,7 @@ Next, create a new console application in this directory and add Raylib-cs and J ```sh dotnet new console dotnet add package Raylib-cs --version 6.1.1 -dotnet add package Jitter2 --version 2.5.0 +dotnet add package Jitter2 --version 2.5.1 ``` You have completed the setup. If you now execute the following command: diff --git a/docs/docs/changelog.md b/docs/docs/changelog.md index 5a084529..31805811 100644 --- a/docs/docs/changelog.md +++ b/docs/docs/changelog.md @@ -4,6 +4,10 @@ sidebar_position: 5 # Changelog +### Jitter 2.5.1 (2024-12-31) + +- Bugfix in PairHashSet. + ### Jitter 2.5.0 (2024-12-23) - Better utilization of multi core systems. diff --git a/other/GodotDemo/JitterGodot.csproj b/other/GodotDemo/JitterGodot.csproj index 48507a96..f229c025 100644 --- a/other/GodotDemo/JitterGodot.csproj +++ b/other/GodotDemo/JitterGodot.csproj @@ -4,6 +4,6 @@ true - + diff --git a/other/GodotSoftBodies/JitterGodot.csproj b/other/GodotSoftBodies/JitterGodot.csproj index 48507a96..f229c025 100644 --- a/other/GodotSoftBodies/JitterGodot.csproj +++ b/other/GodotSoftBodies/JitterGodot.csproj @@ -4,6 +4,6 @@ true - + diff --git a/src/Jitter2/Collision/DynamicTree.cs b/src/Jitter2/Collision/DynamicTree.cs index edbba714..c00b2909 100644 --- a/src/Jitter2/Collision/DynamicTree.cs +++ b/src/Jitter2/Collision/DynamicTree.cs @@ -552,7 +552,7 @@ private void OverlapCheckAdd(int index, int node) { if (node == index) return; if (!Filter(Nodes[node].Proxy, Nodes[index].Proxy)) return; - PotentialPairs.Add(new PairHashSet.Pair(index, node)); + PotentialPairs.ConcurrentAdd(new PairHashSet.Pair(index, node)); } else { diff --git a/src/Jitter2/Collision/PairHashSet.cs b/src/Jitter2/Collision/PairHashSet.cs index d34452df..918516e9 100644 --- a/src/Jitter2/Collision/PairHashSet.cs +++ b/src/Jitter2/Collision/PairHashSet.cs @@ -25,7 +25,6 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; @@ -107,14 +106,13 @@ public void Reset() } } - public Pair[] Slots = Array.Empty(); + public volatile Pair[] Slots = Array.Empty(); + private volatile int count; // 16384*8/1024 KB = 128 KB public const int MinimumSize = 16384; public const int TrimFactor = 8; - private int count; - public int Count => count; private static int PickSize(int size = -1) @@ -141,107 +139,112 @@ public PairHashSet() private void Resize(int size) { if (Slots.Length == size) return; - Trace.WriteLine($"PairHashSet: Resizing {Slots.Length} -> {size}"); - - var tmp = Slots; - count = 0; - Slots = new Pair[size]; + Trace.WriteLine($"PairHashSet: Resizing {Slots.Length} -> {size}"); - Interlocked.MemoryBarrier(); + var newSlots = new Pair[size]; - for (int i = 0; i < tmp.Length; i++) + for (int i = 0; i < Slots.Length; i++) { - Pair pair = tmp[i]; + Pair pair = Slots[i]; if (pair.ID != 0) { - Add(pair); + int hash = pair.GetHash(); + int hash_i = FindSlot(newSlots, hash, pair.ID); + newSlots[hash_i] = pair; } } + + Interlocked.MemoryBarrier(); + Slots = newSlots; + } + + private int FindSlot(Pair[] slots, int hash, long id) + { + int modder = slots.Length - 1; + + hash &= modder; + + while (true) + { + if (slots[hash].ID == 0 || slots[hash].ID == id) return hash; + hash = (hash + 1) & modder; + } } - /// - /// Adds a pair to the hash set if it does not already exist. - /// - /// - /// This method is thread-safe and can be called concurrently from multiple threads. - /// However, it does NOT provide thread safety for other operations like . - /// Ensure external synchronization if other operations are used concurrently. - /// - /// The pair to add. - /// - /// true if the pair was added successfully; false if it already exists. - /// public bool Add(Pair pair) { int hash = pair.GetHash(); - bool overwriteResult = false; + int hash_i = FindSlot(Slots, hash, pair.ID); - try_again: + if (Slots[hash_i].ID == 0) + { + Slots[hash_i] = pair; + Interlocked.Increment(ref count); - var originalSlots = Slots; + if (Slots.Length < 2 * count) + { + Resize(PickSize(Slots.Length * 2)); + } + + return true; + } + + return false; + } + + private Jitter2.Parallelization.ReaderWriterLock rwLock; + + internal void ConcurrentAdd(Pair pair) + { + // TODO: implement a better lock-free version + + int hash = pair.GetHash(); + + rwLock.EnterReadLock(); fixed (Pair* slotsPtr = Slots) { while (true) { - int hash_i = FindSlot(originalSlots, hash, pair.ID); + int hash_i = FindSlot(Slots, hash, pair.ID); + Pair* slotPtr = &slotsPtr[hash_i]; if (slotPtr->ID == pair.ID) { - return overwriteResult; + rwLock.ExitReadLock(); + return; } if (Interlocked.CompareExchange(ref *(long*)slotPtr, *(long*)&pair, 0) == 0) { - if (originalSlots != Slots) - { - // Item was added to the wrong array. - overwriteResult = true; - goto try_again; - } - Interlocked.Increment(ref count); - if (Slots.Length < 2 * Count) + rwLock.ExitReadLock(); + + if (Slots.Length < 2 * count) { - lock (Slots) + rwLock.EnterWriteLock(); + // check if another thread already did the work for us + if (Slots.Length < 2 * count) { - // check if another thread already did the work for us - if (Slots.Length < 2 * Count) - { - Resize(PickSize(Slots.Length * 2)); - } + Resize(PickSize(Slots.Length * 2)); } + rwLock.ExitWriteLock(); } - return true; // Successfully added the pair - + return; } } // while } // fixed } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int FindSlot(Pair[] slots, int hash, long id) - { - int lmodder = slots.Length - 1; - - hash &= lmodder; - - while (true) - { - if (Slots[hash].ID == 0 || Slots[hash].ID == id) return hash; - hash = (hash + 1) & lmodder; - } - } - public bool Remove(int slot) { - int lmodder = Slots.Length - 1; + int modder = Slots.Length - 1; if (Slots[slot].ID == 0) { @@ -252,14 +255,14 @@ public bool Remove(int slot) while (true) { - hash_j = (hash_j + 1) & lmodder; + hash_j = (hash_j + 1) & modder; if (Slots[hash_j].ID == 0) { break; } - int hash_k = Slots[hash_j].GetHash() & lmodder; + int hash_k = Slots[hash_j].GetHash() & modder; // https://en.wikipedia.org/wiki/Open_addressing if ((hash_j > slot && (hash_k <= slot || hash_k > hash_j)) || @@ -271,11 +274,11 @@ public bool Remove(int slot) } Slots[slot] = Pair.Zero; - count -= 1; + Interlocked.Decrement(ref count); - if (Slots.Length > MinimumSize && Count * TrimFactor < Slots.Length) + if (Slots.Length > MinimumSize && count * TrimFactor < Slots.Length) { - Resize(PickSize(Count * 2)); + Resize(PickSize(count * 2)); } return true;