From 35f0b1c26e8c2a6dddfd7d7031162063cebbae20 Mon Sep 17 00:00:00 2001 From: Harald Csaszar Date: Mon, 19 Jan 2026 21:44:44 +0100 Subject: [PATCH] [unity] Added automatic load balancing for threading system for improved performance. Closes #3012. --- CHANGELOG.md | 2 + .../Editor/Utility/BuildSettings.cs | 1 + .../spine-unity/Editor/Utility/Preferences.cs | 12 + .../Editor/Windows/SpinePreferences.cs | 13 + .../spine-unity/Threading/CircularArray.cs | 67 +++ .../Threading/CircularArray.cs.meta | 2 + .../Threading/LockFreeSPSCQueue.cs | 2 +- .../Threading/LockFreeWorkStealingDeque.cs | 141 ++++++ .../LockFreeWorkStealingDeque.cs.meta | 2 + .../LockFreeWorkStealingWorkerPool.cs | 143 ++++++ .../LockFreeWorkStealingWorkerPool.cs.meta | 2 + .../Threading/LockFreeWorkerPool.cs | 2 +- .../Threading/SkeletonUpdateSystem.cs | 418 +++++++++++------- spine-unity/Assets/Spine/package.json | 2 +- 14 files changed, 649 insertions(+), 160 deletions(-) create mode 100644 spine-unity/Assets/Spine/Runtime/spine-unity/Threading/CircularArray.cs create mode 100644 spine-unity/Assets/Spine/Runtime/spine-unity/Threading/CircularArray.cs.meta create mode 100644 spine-unity/Assets/Spine/Runtime/spine-unity/Threading/LockFreeWorkStealingDeque.cs create mode 100644 spine-unity/Assets/Spine/Runtime/spine-unity/Threading/LockFreeWorkStealingDeque.cs.meta create mode 100644 spine-unity/Assets/Spine/Runtime/spine-unity/Threading/LockFreeWorkStealingWorkerPool.cs create mode 100644 spine-unity/Assets/Spine/Runtime/spine-unity/Threading/LockFreeWorkStealingWorkerPool.cs.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index 0639860cb..9f577179e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -364,6 +364,8 @@ - Added `SkeletonUpdateSystem.Instance.GroupRenderersBySkeletonType` and `GroupAnimationBySkeletonType` properties. Defaults to disabled. Later when smart partitioning is implemented, enabling this parameter might slightly improve cache locality. Until then having it enabled combined with different skeleton complexity would lead to worse load balancing. - Added previously missing editor drag & drop skeleton instantiation option *SkeletonGraphic (UI) Mecanim* combining components `SkeletonGraphic` and `SkeletonMecanim`. - Added define `SPINE_DISABLE_THREADING` to disable threaded animation and mesh generation entirely, removing the respective code. This define can be set as `Scripting Define Symbols` globally or for selective build profiles where desired. + - Added automatic load balancing (work stealing) for improved performance when using threaded animation and mesh generation, enabled by default. Load balancing can be disabled via a new Spine preferences parameter `Threading Defaults - Load Balancing` setting a build define accordingly. + Additional configuration parameters `SkeletonUpdateSystem.UpdateChunksPerThread` and `LateUpdateChunksPerThread` are available to fine-tune the chunk count for load balancing. A minimum of 8 chunks is recommended with load balancing enabled. Higher values add higher overhead with potentially detrimental effect on performance. - **Deprecated** diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/BuildSettings.cs b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/BuildSettings.cs index 38d8cbf4a..01c4db818 100644 --- a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/BuildSettings.cs +++ b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/BuildSettings.cs @@ -86,6 +86,7 @@ namespace Spine.Unity.Editor { public const string SPINE_ALLOW_UNSAFE_CODE = "SPINE_ALLOW_UNSAFE"; public const string SPINE_AUTO_UPGRADE_COMPONENTS_OFF = "SPINE_AUTO_UPGRADE_COMPONENTS_OFF"; public const string SPINE_ENABLE_THREAD_PROFILING = "SPINE_ENABLE_THREAD_PROFILING"; + public const string SPINE_DISABLE_LOAD_BALANCING = "SPINE_DISABLE_LOAD_BALANCING"; static bool IsInvalidGroup (BuildTargetGroup group) { int gi = (int)group; diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/Preferences.cs b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/Preferences.cs index ad0739137..0c03be716 100644 --- a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/Preferences.cs +++ b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/Preferences.cs @@ -512,6 +512,18 @@ namespace Spine.Unity.Editor { () => RuntimeSettings.UseThreadedAnimation, value => RuntimeSettings.UseThreadedAnimation = value, new GUIContent("Threaded Animation", "Global setting for the equally named SkeletonAnimation and SkeletonGraphic Inspector parameter.")); +#if SPINE_DISABLE_LOAD_BALANCING + bool loadBalancingEnabled = false; +#else + bool loadBalancingEnabled = true; +#endif + using (new GUILayout.HorizontalScope()) { + EditorGUILayout.PrefixLabel(new GUIContent("Load Balancing", + "Enable load balancing to better utilize threads." + + "Only has an effect when using threaded animation or threaded mesh generation.")); + EnableDisableDefineButtons(SpineBuildEnvUtility.SPINE_DISABLE_LOAD_BALANCING, loadBalancingEnabled, invert: true); + } + #if ALLOWS_CUSTOM_PROFILING #if SPINE_ENABLE_THREAD_PROFILING bool threadProfilingEnabled = true; diff --git a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Windows/SpinePreferences.cs b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Windows/SpinePreferences.cs index bf73e1aca..5d05f0841 100644 --- a/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Windows/SpinePreferences.cs +++ b/spine-unity/Assets/Spine/Editor/spine-unity/Editor/Windows/SpinePreferences.cs @@ -522,6 +522,19 @@ namespace Spine.Unity.Editor { () => RuntimeSettings.UseThreadedAnimation, value => RuntimeSettings.UseThreadedAnimation = value, new GUIContent("Threaded Animation", "Global setting for the equally named SkeletonAnimation and SkeletonGraphic Inspector parameter.")); +#if SPINE_DISABLE_LOAD_BALANCING + bool loadBalancingEnabled = false; +#else + bool loadBalancingEnabled = true; +#endif + using (new GUILayout.HorizontalScope()) { + EditorGUILayout.PrefixLabel(new GUIContent("Load Balancing", + "Enable load balancing to better utilize threads." + + "Only has an effect when using threaded animation or threaded mesh generation.")); + SpineEditorUtilities.EnableDisableDefineButtons(SpineBuildEnvUtility.SPINE_DISABLE_LOAD_BALANCING, + loadBalancingEnabled, invert: true); + } + #if ALLOWS_CUSTOM_PROFILING #if SPINE_ENABLE_THREAD_PROFILING bool threadProfilingEnabled = true; diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/CircularArray.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/CircularArray.cs new file mode 100644 index 000000000..d0fab40c2 --- /dev/null +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/CircularArray.cs @@ -0,0 +1,67 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated July 28, 2023. Replaces all prior versions. + * + * Copyright (c) 2013-2025, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software or + * otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE + * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System; +using System.Threading; + +/// +/// An array with wrap-around access for LockFreeWorkStealingDeque based on the paper +/// "Dynamic Circular Work-Stealing Deque", authors David Chase and Yossi Lev. +/// https://www.dre.vanderbilt.edu/~schmidt/PDF/work-stealing-dequeue.pdf +/// Modified to support negative indices. +/// +public class CircularArray { + private int size; + private T[] segment; + private uint mask; + + public int Size { get { return size; } } + + public CircularArray (int sizePoT) { + this.size = sizePoT; + segment = new T[sizePoT]; + mask = (uint)(sizePoT - 1); + } + + public T Get (int i) { + return this.segment[((uint)i) & mask]; + } + + public void Put (int i, T item) { + this.segment[((uint)i) & mask] = item; + } + + public CircularArray Grow (int b, int t, int newSizePoT) { + CircularArray a = new CircularArray(newSizePoT); + for (int i = t; i < b; i++) { + a.Put(i, this.Get(i)); + } + return a; + } +} diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/CircularArray.cs.meta b/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/CircularArray.cs.meta new file mode 100644 index 000000000..c49c6dfcf --- /dev/null +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/CircularArray.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 347b87d200483c24aaa3764e9eaf2c8f \ No newline at end of file diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/LockFreeSPSCQueue.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/LockFreeSPSCQueue.cs index 1353690f3..c2cc6b959 100644 --- a/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/LockFreeSPSCQueue.cs +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/LockFreeSPSCQueue.cs @@ -2,7 +2,7 @@ * Spine Runtimes License Agreement * Last updated July 28, 2023. Replaces all prior versions. * - * Copyright (c) 2013-2024, Esoteric Software LLC + * Copyright (c) 2013-2025, Esoteric Software LLC * * Integration of the Spine Runtimes into software or otherwise creating * derivative works of the Spine Runtimes is permitted under the terms and diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/LockFreeWorkStealingDeque.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/LockFreeWorkStealingDeque.cs new file mode 100644 index 000000000..11bcf4d7c --- /dev/null +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/LockFreeWorkStealingDeque.cs @@ -0,0 +1,141 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated July 28, 2023. Replaces all prior versions. + * + * Copyright (c) 2013-2025, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software or + * otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE + * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +using System.Threading; + +/// +/// A generic lock-free deque supporting work-stealing based on the paper +/// "Dynamic Circular Work-Stealing Deque", authors David Chase and Yossi Lev +/// https://www.dre.vanderbilt.edu/~schmidt/PDF/work-stealing-dequeue.pdf. +/// Requires that Push and Pop are called from the same thread. +/// Simplified by not supporting growing the array size during a Push, +/// in our usage scenario we populate tasks ahead of time before the first Pop. +/// +public class LockFreeWorkStealingDeque { + public static readonly T Empty = default(T); + public static readonly T Abort = default(T); + + private /*volatile*/ CircularArray activeArray; + private volatile int bottom = 0; + private volatile int top = 0; + + public int Capacity { get { return activeArray.Size; } } + + public LockFreeWorkStealingDeque (int capacity) { + capacity = UnityEngine.Mathf.NextPowerOfTwo(capacity); + activeArray = new CircularArray(capacity); + bottom = 0; + top = 0; + } + + /// Push an element (at the bottom), has to be called by owner of the deque, not a thief. + public void Push (T item) { + int b = bottom; + int t = top; + CircularArray a = this.activeArray; + int size = b - t; + if (size >= a.Size - 1) { + a = a.Grow(b, t, a.Size * 2); + this.activeArray = a; + } + a.Put(b, item); + bottom = b + 1; + } + + /// Non-standard addition for ahead-of-time pushing to maintain queue FIFO order. + /// Push an element at the top, must only be called before any other thread calls Push, Pop or Steal. + public void PushTop (T item) { + int b = bottom; + int t = top; + CircularArray a = this.activeArray; + int size = b - t; + if (size >= a.Size - 1) { + a = a.Grow(b, t, a.Size * 2); + this.activeArray = a; + } + int newT = t - 1; + a.Put(newT, item); + top = newT; + } + + /// + /// Makes a different worker than the owner steal an element (from the top). + /// Returns false if empty. + /// + public bool Steal (out T item) { + int t = top; + int b = bottom; + CircularArray a = this.activeArray; + int size = b - t; + if (size <= 0) { + item = Empty; + return false; + } + T o = a.Get(t); + // increment top + if (Interlocked.CompareExchange(ref top, t + 1, t) != t) { + item = Abort; + return false; + } + item = o; + return true; + } + + /// Pop an element (from the bottom), has to be called by owner of the deque, not a thief. + /// false if empty. + public bool Pop (out T item) { + int b = bottom; + CircularArray a = this.activeArray; + --b; + this.bottom = b; + int t = top; + int size = b - t; + if (size < 0) { + bottom = t; + item = Empty; + return false; + } + T o = a.Get(b); + if (size > 0) { + item = o; + return true; + } + + bool wasSuccessful = true; + if (Interlocked.CompareExchange(ref top, t + 1, t) != t) { + item = Empty; + wasSuccessful = false; + } + else { + item = o; + } + bottom = t + 1; + return wasSuccessful; + } +} \ No newline at end of file diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/LockFreeWorkStealingDeque.cs.meta b/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/LockFreeWorkStealingDeque.cs.meta new file mode 100644 index 000000000..1c0022f2d --- /dev/null +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/LockFreeWorkStealingDeque.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ac7c706babeba4847a4a99f0c35ce0fe \ No newline at end of file diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/LockFreeWorkStealingWorkerPool.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/LockFreeWorkStealingWorkerPool.cs new file mode 100644 index 000000000..9d9c24560 --- /dev/null +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/LockFreeWorkStealingWorkerPool.cs @@ -0,0 +1,143 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated July 28, 2023. Replaces all prior versions. + * + * Copyright (c) 2013-2025, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software or + * otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE + * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +#define ENABLE_WORK_STEALING + +using System; +using System.Collections.Generic; +using System.Threading; +#if SPINE_ENABLE_THREAD_PROFILING +using UnityEngine.Profiling; +#endif + +/// Class to distribute work items like ThreadPool.QueueUserWorkItem but keep the same tasks at the same thread +/// across frames, increasing core affinity (and in some scenarios with lower cache pressure on secondary cores, +/// perhaps even reduce cache eviction). +public class LockFreeWorkStealingWorkerPool : IDisposable { + public class Task { + public T parameters; + public Action function; + } + + private readonly int _threadCount; + private readonly Thread[] _threads; + private readonly LockFreeWorkStealingDeque[] _taskQueues; + private readonly AutoResetEvent[] _taskAvailable; + private volatile bool _running = true; + + public LockFreeWorkStealingWorkerPool (int threadCount, int queueCapacity = 8) { + _threadCount = threadCount; + _threads = new Thread[_threadCount]; + _taskQueues = new LockFreeWorkStealingDeque[_threadCount]; + _taskAvailable = new AutoResetEvent[_threadCount]; + + for (int i = 0; i < _threadCount; i++) { + _taskQueues[i] = new LockFreeWorkStealingDeque(queueCapacity); + _taskAvailable[i] = new AutoResetEvent(false); + int index = i; // Capture the index for the thread + _threads[i] = new Thread(() => WorkerLoop(index)); + } + for (int i = 0; i < _threadCount; i++) { + _threads[i].Start(); + } + } + + /// Enqueues a task item if there is space available, but does + /// not start processing until is called. + /// True if the item was successfully enqueued, false otherwise. + public bool EnqueueTask (int threadIndex, Task task) { + if (threadIndex < 0 || threadIndex >= _threadCount) + throw new ArgumentOutOfRangeException("threadIndex"); + + _taskQueues[threadIndex].PushTop(task); + return true; + } + + /// + /// Call this method after to start processing all enqueued tasks. + /// This limitation comes from LockFreeWorkStealingDeque requiring the same thread calling Push and Pop, + /// which would not be the case here. + /// + /// Limits the number of active worker threads. Note that when work stealing is + /// enabled, empty threads steal tasks from other threads even if no tasks were originally enqueued at them. + public void AllowTaskProcessing (int numAsyncThreads) { + for (int t = 0; t < numAsyncThreads; ++t) + _taskAvailable[t].Set(); + } + + private void WorkerLoop (int threadIndex) { +#if SPINE_ENABLE_THREAD_PROFILING + Profiler.BeginThreadProfiling("Spine Threads", "Spine Thread " + threadIndex); +#endif + while (_running) { + _taskAvailable[threadIndex].WaitOne(); + Task task = null; + bool success; + do { + success = _taskQueues[threadIndex].Pop(out task); + if (success) { + task.function(task.parameters, threadIndex); + } else { + #if ENABLE_WORK_STEALING + int stealThreadIndex = (threadIndex + 1) % _threadCount; + while (stealThreadIndex != threadIndex) { // circle complete + while (true) { + task = null; + bool stealSuccessful = _taskQueues[stealThreadIndex].Steal(out task); + if (!stealSuccessful) + break; + task.function(task.parameters, threadIndex); + } + stealThreadIndex = (stealThreadIndex + 1) % _threadCount; + } + #endif + } + } while (success); + } +#if SPINE_ENABLE_THREAD_PROFILING + Profiler.EndThreadProfiling(); +#endif + } + + public void Dispose () { + _running = false; + + for (int i = 0; i < _threadCount; i++) { + _taskAvailable[i].Set(); // Wake up threads to exit + } + + foreach (var thread in _threads) { + thread.Join(); + } + + for (int i = 0; i < _threadCount; i++) { + _taskAvailable[i].Close(); + } + } +} diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/LockFreeWorkStealingWorkerPool.cs.meta b/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/LockFreeWorkStealingWorkerPool.cs.meta new file mode 100644 index 000000000..e69d9293f --- /dev/null +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/LockFreeWorkStealingWorkerPool.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 14b683fce49a5a641b55e8af49df35a4 \ No newline at end of file diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/LockFreeWorkerPool.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/LockFreeWorkerPool.cs index 77327e256..ce4ec8256 100644 --- a/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/LockFreeWorkerPool.cs +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/LockFreeWorkerPool.cs @@ -2,7 +2,7 @@ * Spine Runtimes License Agreement * Last updated July 28, 2023. Replaces all prior versions. * - * Copyright (c) 2013-2024, Esoteric Software LLC + * Copyright (c) 2013-2025, Esoteric Software LLC * * Integration of the Spine Runtimes into software or otherwise creating * derivative works of the Spine Runtimes is permitted under the terms and diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/SkeletonUpdateSystem.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/SkeletonUpdateSystem.cs index 7d45e9d83..bbca35c90 100644 --- a/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/SkeletonUpdateSystem.cs +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/SkeletonUpdateSystem.cs @@ -2,7 +2,7 @@ * Spine Runtimes License Agreement * Last updated July 28, 2023. Replaces all prior versions. * - * Copyright (c) 2013-2024, Esoteric Software LLC + * Copyright (c) 2013-2025, Esoteric Software LLC * * Integration of the Spine Runtimes into software or otherwise creating * derivative works of the Spine Runtimes is permitted under the terms and @@ -32,21 +32,35 @@ #define USE_THREADED_ANIMATION_UPDATE // requires USE_THREADED_SKELETON_UPDATE enabled #endif +#if !SPINE_DISABLE_LOAD_BALANCING +#define ENABLE_WORK_STEALING // load balancing, enabled improves performance, distributes work to otherwise idle threads. +#endif + #define READ_VOLATILE_ONCE #define DONT_WAIT_FOR_ALL_LATEUPDATE_TASKS // enabled improves performance a bit. //#define RUN_ALL_ON_MAIN_THREAD // for profiling comparison only -//#define RUN_NO_ANIMATION_UPDATE_ON_MAIN_THREAD // actual configuration option. depends, measured slightly better when disabled. -#define RUN_NO_SKELETON_LATEUPDATE_ON_MAIN_THREAD // actual configuration option, recommended enabled +// actual configuration option, does not matter with mainThreadUpdateCallbacks enabled. +// measured slightly better when disabled with disabled work-stealing (load balancing), better when enabled with work-stealing enabled +#if ENABLE_WORK_STEALING +#define RUN_NO_ANIMATION_UPDATE_ON_MAIN_THREAD +#endif -#if NET_4_6 +#if ENABLE_WORK_STEALING +#define REQUIRES_MORE_CHUNKS +#else +#define RUN_NO_SKELETON_LATEUPDATE_ON_MAIN_THREAD // actual configuration option, recommended enabled when not using work stealing +#endif + +#if NET_STANDARD_2_0 || NET_STANDARD_2_1 || NET_4_6 #define HAS_MANUAL_RESET_EVENT_SLIM #endif #if USE_THREADED_SKELETON_UPDATE using System; +using System.Collections; using System.Collections.Generic; using System.Threading; using UnityEngine; @@ -62,9 +76,13 @@ using ResetEvent = System.Threading.ManualResetEvent; #endif namespace Spine.Unity { +#if ENABLE_WORK_STEALING + using WorkerPool = LockFreeWorkStealingWorkerPool; + using WorkerPoolTask = LockFreeWorkStealingWorkerPool.Task; +#else using WorkerPool = LockFreeWorkerPool; using WorkerPoolTask = LockFreeWorkerPool.Task; - +#endif [DefaultExecutionOrder(0)] public class SkeletonUpdateSystem : MonoBehaviour { @@ -72,6 +90,14 @@ namespace Spine.Unity { const int TimeoutIterationCount = 10000; +#if REQUIRES_MORE_CHUNKS + public int UpdateChunksPerThread = 8; + public int LateUpdateChunksPerThread = 8; +#else + public int UpdateChunksPerThread = 1; + public int LateUpdateChunksPerThread = 1; +#endif + public static SkeletonUpdateSystem Instance { get { if (singletonInstance == null) { @@ -124,10 +150,17 @@ namespace Spine.Unity { public struct SkeletonUpdateRange { public int rangeStart; public int rangeEndExclusive; + public int taskIndex; public int frameCount; public UpdateTiming updateTiming; } + public struct SkeletonPartitionRange { + public int rangeStart; + public int rangeEndExclusive; + public int threadIndex; + } + public List skeletonAnimationsUpdate = new List(); public List skeletonAnimationsFixedUpdate = new List(); public List skeletonAnimationsLateUpdate = new List(); @@ -136,14 +169,15 @@ namespace Spine.Unity { public WorkerPool workerPool; + ExposedList taskPartitionsUpdate = null; + ExposedList taskPartitionsLateUpdate = null; + public List updateDone = new List(4); public List lateUpdateDone = new List(4); #if DONT_WAIT_FOR_ALL_LATEUPDATE_TASKS - protected int[] rendererStartIndex; - protected int[] rendererEndIndexExclusive; - volatile protected int[] skeletonsLateUpdatedAtThread; - protected int[] mainThreadProcessed; + volatile protected int[] skeletonsLateUpdatedAtTask; + protected int[] mainThreadProcessedAtTask; public AutoResetEvent lateUpdateWorkAvailable; #endif protected Exception[] exceptions; @@ -270,16 +304,17 @@ namespace Spine.Unity { #if RUN_ALL_ON_MAIN_THREAD int numAsyncThreads = 0; #elif RUN_NO_ANIMATION_UPDATE_ON_MAIN_THREAD - int numAsyncThreads = numThreads; + int numAsyncThreads = mainThreadUpdateCallbacks ? numThreads - 1 : numThreads; #else int numAsyncThreads = numThreads - 1; #endif - + int tasksPerThread = UpdateChunksPerThread; + int numTasks = numThreads * tasksPerThread; if (workerPool == null) - workerPool = new WorkerPool(numThreads); - if (genericSkeletonTasks == null) { - genericSkeletonTasks = new WorkerPoolTask[numThreads]; - for (int t = 0; t < numThreads; ++t) { + workerPool = new WorkerPool(numThreads, tasksPerThread + 1); + if (genericSkeletonTasks == null || genericSkeletonTasks.Length < numTasks) { + genericSkeletonTasks = new WorkerPoolTask[numTasks]; + for (int t = 0; t < genericSkeletonTasks.Length; ++t) { genericSkeletonTasks[t] = new WorkerPoolTask(); } } @@ -288,14 +323,16 @@ namespace Spine.Unity { profilerSamplerUpdate = new CustomSampler[numThreads]; } #endif + int endIndexThreaded; + int numAvailableThreads = mainThreadUpdateCallbacks ? numAsyncThreads : UsedThreadCount; + PartitionTasks(ref taskPartitionsUpdate, out endIndexThreaded, tasksPerThread, skeletons.Count, + numAsyncThreads, numAvailableThreads); for (int t = 0; t < updateDone.Count; ++t) { updateDone[t].Reset(); } - if (updateDone.Count != numThreads) { - for (int t = updateDone.Count; t < numThreads; ++t) { - updateDone.Add(new ResetEvent(false)); - } + for (int t = updateDone.Count; t < numTasks; ++t) { + updateDone.Add(new ResetEvent(false)); } if (exceptions == null) { @@ -304,9 +341,7 @@ namespace Spine.Unity { } numExceptionsSet = 0; - int rangePerThread = Mathf.CeilToInt((float)skeletons.Count / (float)numThreads); int skeletonEnd = skeletons.Count; - int endIndexThreaded = Math.Min(skeletonEnd, rangePerThread * numAsyncThreads); SkeletonAnimationBase.ExternalDeltaTime = Time.deltaTime; SkeletonAnimationBase.ExternalUnscaledDeltaTime = Time.unscaledDeltaTime; @@ -318,52 +353,94 @@ namespace Spine.Unity { } #else if (!mainThreadUpdateCallbacks) - UpdateAsyncThreadedCallbacks(skeletons, updateTiming, - numThreads, numAsyncThreads, rangePerThread, skeletonEnd); + UpdateAsyncThreadedCallbacks(skeletons, updateTiming, taskPartitionsUpdate, + numAsyncThreads, skeletonEnd); else - UpdateAsyncSplitMainThreadCallbacks(skeletons, updateTiming, - numAsyncThreads, rangePerThread, skeletonEnd, endIndexThreaded); + UpdateAsyncSplitMainThreadCallbacks(skeletons, updateTiming, taskPartitionsUpdate, + numAsyncThreads, skeletonEnd); #endif MainThreadAfterUpdate(skeletons, skeletonEnd); } + protected void PartitionTasks(ref ExposedList taskPartitions, out int outAsyncEndExclusive, + int tasksPerThread, int skeletonCount, int numAsyncThreads, int numAvailableThreads) { + + int numAsyncTasks = numAsyncThreads * tasksPerThread; + if (taskPartitions == null) { + taskPartitions = new ExposedList(numAsyncTasks); + } + if (taskPartitions.Count != numAsyncTasks) { + taskPartitions.Resize(numAsyncTasks); + } + + int rangePerThread = Mathf.CeilToInt((float)skeletonCount / (float)numAvailableThreads); + int rangePerTask = Math.Max(1, Mathf.CeilToInt((float)rangePerThread / (float)tasksPerThread)); + + int totalAsyncTasks = 0; + int threadStart = 0; + int threadEnd = Mathf.Min(rangePerThread, skeletonCount); + SkeletonPartitionRange[] partitionItems = taskPartitions.Items; + for (int threadIndex = 0; threadIndex < numAsyncThreads; ++threadIndex) { + int start = threadStart; + int end = Mathf.Min(start + rangePerTask, threadEnd); + for (int t = 0; t < tasksPerThread; ++t) { + partitionItems[totalAsyncTasks++] = new SkeletonPartitionRange() { + rangeStart = start, + rangeEndExclusive = end, + threadIndex = threadIndex + }; + start = end; + end = Mathf.Min(end + rangePerTask, threadEnd); + } + threadStart = threadEnd; + threadEnd = Mathf.Min(threadEnd + rangePerThread, skeletonCount); + } + outAsyncEndExclusive = threadStart; // threadStart == previous threadEnd + } + protected void UpdateAsyncThreadedCallbacks (List skeletons, UpdateTiming timing, - int numThreads, int numAsyncThreads, int rangePerThread, - int skeletonEnd) { + ExposedList asyncTaskPartitions, int numAsyncThreads, int skeletonEnd) { - int start = 0; - int end = Mathf.Min(rangePerThread, skeletonEnd); - - for (int t = 0; t < numThreads; ++t) { + SkeletonPartitionRange[] asyncPartitionsItems = asyncTaskPartitions.Items; + for (int taskIndex = 0, count = asyncTaskPartitions.Count; taskIndex < count; ++taskIndex) { + SkeletonPartitionRange partition = asyncPartitionsItems[taskIndex]; + if (partition.rangeStart == partition.rangeEndExclusive) { + updateDone[taskIndex].Set(); + continue; + } var range = new SkeletonUpdateRange() { - rangeStart = start, - rangeEndExclusive = end, + rangeStart = partition.rangeStart, + rangeEndExclusive = partition.rangeEndExclusive, + taskIndex = taskIndex, frameCount = Time.frameCount, updateTiming = timing }; - - if (t < numAsyncThreads) { - UpdateSkeletonsAsync(range, t); - } else { - // this main thread does some work as well, otherwise it's only waiting. - UpdateSkeletonsSynchronous(skeletons, range); - } - - start = end; - if (start >= skeletonEnd) { - while (++t < numAsyncThreads) - updateDone[t].Set(); - break; - } - end = Mathf.Min(end + rangePerThread, skeletonEnd); + UpdateSkeletonsAsync(range, partition.threadIndex); } - - WaitForThreadUpdateTasks(numAsyncThreads); +#if ENABLE_WORK_STEALING + workerPool.AllowTaskProcessing(numAsyncThreads); +#endif + SkeletonPartitionRange lastAsyncPartition = asyncPartitionsItems[asyncTaskPartitions.Count - 1]; + if (lastAsyncPartition.rangeEndExclusive < skeletonEnd) { + // this main thread does some work as well, otherwise it's only waiting. + var range = new SkeletonUpdateRange() { + rangeStart = lastAsyncPartition.rangeEndExclusive, + rangeEndExclusive = skeletonEnd, + taskIndex = -1, + frameCount = Time.frameCount, + updateTiming = timing + }; + UpdateSkeletonsSynchronous(skeletons, range); + } + WaitForThreadUpdateTasks(asyncTaskPartitions.Count); } protected void UpdateAsyncSplitMainThreadCallbacks (List skeletons, UpdateTiming timing, - int numAsyncThreads, int rangePerThread, - int skeletonEnd, int endIndexThreaded) { + ExposedList asyncTaskPartitions, int numAsyncThreads, int skeletonEnd) { + + SkeletonPartitionRange[] asyncPartitionsItems = asyncTaskPartitions.Items; + SkeletonPartitionRange lastAsyncPartition = asyncPartitionsItems[asyncTaskPartitions.Count - 1]; + int endIndexThreaded = lastAsyncPartition.rangeEndExclusive; if (splitUpdateMethod == null) { splitUpdateMethod = new CoroutineIterator[skeletons.Count]; @@ -377,43 +454,40 @@ namespace Spine.Unity { bool anyWorkLeft; int timeoutCounter = 0; do { - int start = 0; - int end = Mathf.Min(rangePerThread, skeletonEnd); - for (int t = 0; t < numAsyncThreads; ++t) { - var range = new SkeletonUpdateRange() { - rangeStart = start, - rangeEndExclusive = end, - frameCount = Time.frameCount, - updateTiming = timing - }; - - UpdateSkeletonsAsyncSplit(range, t); - - start = end; - if (start >= skeletonEnd) { - while (++t < numAsyncThreads) { - updateDone[t].Set(); - } - break; + for (int taskIndex = 0, count = asyncTaskPartitions.Count; taskIndex < count; ++taskIndex) { + SkeletonPartitionRange partition = asyncPartitionsItems[taskIndex]; + if (partition.rangeStart == partition.rangeEndExclusive) { + updateDone[taskIndex].Set(); + continue; } - end = Mathf.Min(end + rangePerThread, skeletonEnd); - } - - // main thread - if (isFirstIteration && start != skeletonEnd) { var range = new SkeletonUpdateRange() { - rangeStart = start, - rangeEndExclusive = end, + rangeStart = partition.rangeStart, + rangeEndExclusive = partition.rangeEndExclusive, + taskIndex = taskIndex, frameCount = Time.frameCount, updateTiming = timing }; + UpdateSkeletonsAsyncSplit(range, partition.threadIndex); + } +#if ENABLE_WORK_STEALING + workerPool.AllowTaskProcessing(numAsyncThreads); +#endif + // main thread + if (isFirstIteration && lastAsyncPartition.rangeEndExclusive < skeletonEnd) { // this main thread does complete update work in the first iteration, otherwise it's only waiting. + var range = new SkeletonUpdateRange() { + rangeStart = lastAsyncPartition.rangeEndExclusive, + rangeEndExclusive = skeletonEnd, + taskIndex = -1, + frameCount = Time.frameCount, + updateTiming = timing + }; UpdateSkeletonsSynchronous(skeletons, range); } // wait for all threaded tasks - WaitForThreadUpdateTasks(numAsyncThreads); - for (int t = 0; t < updateDone.Count; ++t) { + WaitForThreadUpdateTasks(asyncTaskPartitions.Count); + for (int t = 0; t < asyncTaskPartitions.Count; ++t) { updateDone[t].Reset(); } @@ -463,11 +537,13 @@ namespace Spine.Unity { #else int numAsyncThreads = numThreads - 1; #endif + int tasksPerThread = LateUpdateChunksPerThread; + int numTasks = numThreads * tasksPerThread; if (workerPool == null) - workerPool = new WorkerPool(numThreads); - if (genericSkeletonTasks == null) { - genericSkeletonTasks = new WorkerPoolTask[numThreads]; - for (int t = 0; t < numThreads; ++t) { + workerPool = new WorkerPool(numThreads, tasksPerThread * 2); + if (genericSkeletonTasks == null || genericSkeletonTasks.Length < numTasks) { + genericSkeletonTasks = new WorkerPoolTask[numTasks]; + for (int t = 0; t < genericSkeletonTasks.Length; ++t) { genericSkeletonTasks[t] = new WorkerPoolTask(); } } @@ -476,62 +552,71 @@ namespace Spine.Unity { profilerSamplerLateUpdate = new CustomSampler[numThreads]; } #endif + int endIndexThreaded; +#if DONT_WAIT_FOR_ALL_LATEUPDATE_TASKS + int numAvailableThreads = numAsyncThreads; +#else + int numAvailableThreads = UsedThreadCount; +#endif + PartitionTasks(ref taskPartitionsLateUpdate, out endIndexThreaded, tasksPerThread, skeletonRenderers.Count, + numAsyncThreads, numAvailableThreads); + ExposedList asyncTaskPartitions = taskPartitionsLateUpdate; + int numAsyncTasks = asyncTaskPartitions.Count; +#if !DONT_WAIT_FOR_ALL_LATEUPDATE_TASKS for (int t = 0; t < lateUpdateDone.Count; ++t) { lateUpdateDone[t].Reset(); } - if (lateUpdateDone.Count != numThreads) { - for (int t = lateUpdateDone.Count; t < numThreads; ++t) { - lateUpdateDone.Add(new ResetEvent(false)); - } + for (int t = lateUpdateDone.Count; t < numAsyncTasks; ++t) { + lateUpdateDone.Add(new ResetEvent(false)); } +#endif // !DONT_WAIT_FOR_ALL_LATEUPDATE_TASKS - int rangePerThread = Mathf.CeilToInt((float)skeletonRenderers.Count / (float)numThreads); int skeletonEnd = skeletonRenderers.Count; #if DONT_WAIT_FOR_ALL_LATEUPDATE_TASKS - if (skeletonsLateUpdatedAtThread == null) { - skeletonsLateUpdatedAtThread = new int[numThreads]; - mainThreadProcessed = new int[numThreads]; - rendererStartIndex = new int[numThreads]; - rendererEndIndexExclusive = new int[numThreads]; + if (skeletonsLateUpdatedAtTask == null) { + skeletonsLateUpdatedAtTask = new int[numAsyncTasks]; + mainThreadProcessedAtTask = new int[numAsyncTasks]; lateUpdateWorkAvailable = new AutoResetEvent(false); } - for (int t = 0; t < numThreads; ++t) { - rendererStartIndex[t] = rangePerThread * t; - rendererEndIndexExclusive[t] = Mathf.Min(rangePerThread * (t + 1), skeletonEnd); - } - for (int t = 0; t < numAsyncThreads; ++t) { - skeletonsLateUpdatedAtThread[t] = 0; + for (int t = 0; t < numAsyncTasks; ++t) { + skeletonsLateUpdatedAtTask[t] = 0; } #endif - int endIndexThreaded = Math.Min(skeletonEnd, rangePerThread * numAsyncThreads); MainThreadPrepareLateUpdate(endIndexThreaded); - int start = 0; - int end = Mathf.Min(rangePerThread, skeletonEnd); - - for (int t = 0; t < numThreads; ++t) { + SkeletonPartitionRange[] asyncPartitionsItems = asyncTaskPartitions.Items; + for (int taskIndex = 0, count = asyncTaskPartitions.Count; taskIndex < count; ++taskIndex) { + SkeletonPartitionRange partition = asyncPartitionsItems[taskIndex]; + if (partition.rangeStart == partition.rangeEndExclusive) { +#if !DONT_WAIT_FOR_ALL_LATEUPDATE_TASKS + lateUpdateDone[taskIndex].Set(); +#endif + continue; + } var range = new SkeletonUpdateRange() { - rangeStart = start, - rangeEndExclusive = end, + rangeStart = partition.rangeStart, + rangeEndExclusive = partition.rangeEndExclusive, + taskIndex = taskIndex, frameCount = Time.frameCount, updateTiming = UpdateTiming.InLateUpdate }; - - if (t < numAsyncThreads) { - LateUpdateSkeletonsAsync(range, t); - } else { - // this main thread does some work as well, otherwise it's only waiting. - LateUpdateSkeletonsSynchronous(range); - } - - start = end; - if (start >= skeletonEnd) { - while (++t < numAsyncThreads) - lateUpdateDone[t].Set(); - break; - } - end = Mathf.Min(end + rangePerThread, skeletonEnd); + LateUpdateSkeletonsAsync(range, partition.threadIndex); + } +#if ENABLE_WORK_STEALING + workerPool.AllowTaskProcessing(numAsyncThreads); +#endif + SkeletonPartitionRange lastAsyncPartition = asyncPartitionsItems[asyncTaskPartitions.Count - 1]; + if (lastAsyncPartition.rangeEndExclusive < skeletonEnd) { + // this main thread does some work as well, otherwise it's only waiting. + var range = new SkeletonUpdateRange() { + rangeStart = lastAsyncPartition.rangeEndExclusive, + rangeEndExclusive = skeletonEnd, + taskIndex = -1, + frameCount = Time.frameCount, + updateTiming = UpdateTiming.InLateUpdate + }; + LateUpdateSkeletonsSynchronous(range); } #if RUN_ALL_ON_MAIN_THREAD @@ -539,35 +624,38 @@ namespace Spine.Unity { #endif #if DONT_WAIT_FOR_ALL_LATEUPDATE_TASKS - for (int t = 0; t < numAsyncThreads; ++t) { - mainThreadProcessed[t] = 0; + for (int t = 0; t < numAsyncTasks; ++t) { + mainThreadProcessedAtTask[t] = 0; } bool anySkeletonsLeft = false; - bool wasWorkAvailable = false; bool timedOut = false; do { + bool wasWorkAvailable = false; anySkeletonsLeft = false; - for (int t = 0; t < numAsyncThreads; ++t) { - int threadEndIndex = rendererEndIndexExclusive[t] - rendererStartIndex[t]; -#if READ_VOLATILE_ONCE - int updatedAtWorkerThread = skeletonsLateUpdatedAtThread[t]; + for (int t = 0; t < numAsyncTasks; ++t) { + SkeletonPartitionRange partition = asyncPartitionsItems[t]; - while (mainThreadProcessed[t] < updatedAtWorkerThread) { + int rendererStartIndex = partition.rangeStart; + int countAtTask = partition.rangeEndExclusive - rendererStartIndex; +#if READ_VOLATILE_ONCE + int updatedAtWorkerThread = skeletonsLateUpdatedAtTask[t]; + + while (mainThreadProcessedAtTask[t] < updatedAtWorkerThread) { #else while (mainThreadProcessed[t] < skeletonsLateUpdatedAtThread[t]) { #endif wasWorkAvailable = true; - int r = mainThreadProcessed[t] + rendererStartIndex[t]; + int r = mainThreadProcessedAtTask[t] + rendererStartIndex; var skeletonRenderer = this.skeletonRenderers[r]; if (skeletonRenderer.RequiresMeshBufferAssignmentMainThread) skeletonRenderer.UpdateMeshAndMaterialsToBuffers(); - mainThreadProcessed[t]++; + mainThreadProcessedAtTask[t]++; } #if READ_VOLATILE_ONCE - if (updatedAtWorkerThread < threadEndIndex) { + if (updatedAtWorkerThread < countAtTask) { #else - if (skeletonsLateUpdatedAtThread[t] < threadEndIndex) { + if (skeletonsLateUpdatedAtThread[t] < countAtTask) { #endif anySkeletonsLeft = true; } @@ -584,13 +672,11 @@ namespace Spine.Unity { } #else // wait for all threaded task, then process all renderers in main thread - WaitForThreadLateUpdateTasks(numAsyncThreads); + WaitForThreadLateUpdateTasks(numAsyncTasks); // Additional main thread update when the mesh data could not be assigned from worker thread // and has to be assigned from main thread. - int maxNonUpdatedRenderer = Math.Min(rangePerThread * numAsyncThreads, this.skeletonRenderers.Count); - - for (int r = 0; r < maxNonUpdatedRenderer; ++r) { + for (int r = 0; r < endIndexThreaded; ++r) { var skeletonRenderer = this.skeletonRenderers[r]; if (skeletonRenderer.RequiresMeshBufferAssignmentMainThread) skeletonRenderer.UpdateMeshAndMaterialsToBuffers(); @@ -604,14 +690,16 @@ namespace Spine.Unity { } } - private void WaitForThreadUpdateTasks (int numAsyncThreads) { - for (int t = 0, n = numAsyncThreads; t < n; ++t) { + private void WaitForThreadUpdateTasks (int numAsyncTasks) { + for (int t = 0; t < numAsyncTasks; ++t) { int timeoutMilliseconds = 1000; #if HAS_MANUAL_RESET_EVENT_SLIM - updateDone[t].Wait(timeoutMilliseconds); + bool success = updateDone[t].Wait(timeoutMilliseconds); #else // HAS_MANUAL_RESET_EVENT_SLIM - updateDone[t].WaitOne(timeoutMilliseconds); + bool success = updateDone[t].WaitOne(timeoutMilliseconds); #endif // HAS_MANUAL_RESET_EVENT_SLIM + if (!success) + Debug.LogError(string.Format("Waiting for updateDone on main thread ran into a timeout (task index: {0})!", t)); } LogWorkerThreadExceptions(); } @@ -629,16 +717,20 @@ namespace Spine.Unity { } } - private void WaitForThreadLateUpdateTasks (int numAsyncThreads) { - for (int t = 0, n = numAsyncThreads; t < n; ++t) { +#if !DONT_WAIT_FOR_ALL_LATEUPDATE_TASKS + private void WaitForThreadLateUpdateTasks (int numAsyncTasks) { + for (int t = 0; t < numAsyncTasks; ++t) { int timeoutMilliseconds = 1000; #if HAS_MANUAL_RESET_EVENT_SLIM - lateUpdateDone[t].Wait(timeoutMilliseconds); + bool success = lateUpdateDone[t].Wait(timeoutMilliseconds); #else // HAS_MANUAL_RESET_EVENT_SLIM - lateUpdateDone[t].WaitOne(timeoutMilliseconds); + bool success = lateUpdateDone[t].WaitOne(timeoutMilliseconds); #endif // HAS_MANUAL_RESET_EVENT_SLIM + if (!success) + Debug.LogError(string.Format("Waiting for lateUpdateDone on main thread ran into a timeout (task index: {0})!", t)); } } +#endif // !DONT_WAIT_FOR_ALL_LATEUPDATE_TASKS #if SPINE_ENABLE_THREAD_PROFILING CustomSampler[] profilerSamplerUpdate = null; @@ -652,12 +744,12 @@ namespace Spine.Unity { profilerSamplerUpdate[threadIndex] = CustomSampler.Create("Spine Update " + threadIndex); } #endif - WorkerPoolTask task = genericSkeletonTasks[threadIndex]; + WorkerPoolTask task = genericSkeletonTasks[range.taskIndex]; task.parameters = range; task.function = cachedUpdateSkeletonsAsyncImpl; bool enqueueSucceeded; do { - enqueueSucceeded = workerPool.EnqueueTask(threadIndex, genericSkeletonTasks[threadIndex]); + enqueueSucceeded = workerPool.EnqueueTask(threadIndex, task); } while (!enqueueSucceeded); } // avoid allocation, unfortunately this is really necessary @@ -665,11 +757,15 @@ namespace Spine.Unity { static void UpdateSkeletonsAsyncImpl (SkeletonUpdateRange range, int threadIndex) { var instance = Instance; #if SPINE_ENABLE_THREAD_PROFILING + if (instance.profilerSamplerUpdate[threadIndex] == null) { + instance.profilerSamplerUpdate[threadIndex] = CustomSampler.Create("Spine Update " + threadIndex); + } instance.profilerSamplerUpdate[threadIndex].Begin(); #endif int frameCount = range.frameCount; int start = range.rangeStart; int end = range.rangeEndExclusive; + int taskIndex = range.taskIndex; var skeletonAnimations = instance.skeletonAnimationsUpdate; if (range.updateTiming == UpdateTiming.InFixedUpdate) skeletonAnimations = instance.skeletonAnimationsFixedUpdate; else if (range.updateTiming == UpdateTiming.InLateUpdate) skeletonAnimations = instance.skeletonAnimationsLateUpdate; @@ -681,9 +777,9 @@ namespace Spine.Unity { instance.DeferredLogException(exc, skeletonAnimations[r], threadIndex); } } - instance.updateDone[threadIndex].Set(); + instance.updateDone[taskIndex].Set(); #if SPINE_ENABLE_THREAD_PROFILING - instance.profilerSamplerUpdate[threadIndex].End(); + instance.profilerSamplerUpdate[threadIndex].End(); #endif } @@ -699,10 +795,10 @@ namespace Spine.Unity { #endif bool enqueueSucceeded; do { - WorkerPoolTask task = genericSkeletonTasks[threadIndex]; + WorkerPoolTask task = genericSkeletonTasks[range.taskIndex]; task.parameters = range; task.function = cachedUpdateSkeletonsAsyncSplitImpl; - enqueueSucceeded = workerPool.EnqueueTask(threadIndex, genericSkeletonTasks[threadIndex]); + enqueueSucceeded = workerPool.EnqueueTask(threadIndex, task); } while (!enqueueSucceeded); } // avoid allocation, unfortunately this is really necessary @@ -711,6 +807,7 @@ namespace Spine.Unity { int frameCount = range.frameCount; int start = range.rangeStart; int end = range.rangeEndExclusive; + int taskIndex = range.taskIndex; var instance = Instance; var skeletonAnimations = instance.skeletonAnimationsUpdate; if (range.updateTiming == UpdateTiming.InFixedUpdate) skeletonAnimations = instance.skeletonAnimationsFixedUpdate; @@ -719,6 +816,9 @@ namespace Spine.Unity { var splitUpdateMethod = instance.splitUpdateMethod; #if SPINE_ENABLE_THREAD_PROFILING + if (instance.profilerSamplerUpdate[threadIndex] == null) { + instance.profilerSamplerUpdate[threadIndex] = CustomSampler.Create("Spine Update " + threadIndex); + } instance.profilerSamplerUpdate[threadIndex].Begin(); #endif for (int r = start; r < end; ++r) { @@ -731,7 +831,7 @@ namespace Spine.Unity { instance.DeferredLogException(exc, skeletonAnimations[r], threadIndex); } } - instance.updateDone[threadIndex].Set(); + instance.updateDone[taskIndex].Set(); #if SPINE_ENABLE_THREAD_PROFILING instance.profilerSamplerUpdate[threadIndex].End(); @@ -780,24 +880,28 @@ namespace Spine.Unity { } #endif bool enqueueSucceeded; - WorkerPoolTask task = genericSkeletonTasks[threadIndex]; + WorkerPoolTask task = genericSkeletonTasks[range.taskIndex]; task.parameters = range; task.function = cachedLateUpdateSkeletonsAsyncImpl; do { - enqueueSucceeded = workerPool.EnqueueTask(threadIndex, genericSkeletonTasks[threadIndex]); + enqueueSucceeded = workerPool.EnqueueTask(threadIndex, task); } while (!enqueueSucceeded); } static void LateUpdateSkeletonsAsyncImpl (SkeletonUpdateRange range, int threadIndex) { int start = range.rangeStart; int end = range.rangeEndExclusive; + int taskIndex = range.taskIndex; var instance = Instance; #if SPINE_ENABLE_THREAD_PROFILING + if (instance.profilerSamplerLateUpdate[threadIndex] == null) { + instance.profilerSamplerLateUpdate[threadIndex] = CustomSampler.Create("Spine LateUpdate " + threadIndex); + } instance.profilerSamplerLateUpdate[threadIndex].Begin(); #endif #if DONT_WAIT_FOR_ALL_LATEUPDATE_TASKS - instance.skeletonsLateUpdatedAtThread[threadIndex] = 0; + instance.skeletonsLateUpdatedAtTask[taskIndex] = 0; #endif for (int r = start; r < end; ++r) { try { @@ -806,12 +910,12 @@ namespace Spine.Unity { instance.DeferredLogException(exc, instance.skeletonRenderers[r].Component, threadIndex); } #if DONT_WAIT_FOR_ALL_LATEUPDATE_TASKS - Interlocked.Increment(ref instance.skeletonsLateUpdatedAtThread[threadIndex]); + Interlocked.Increment(ref instance.skeletonsLateUpdatedAtTask[taskIndex]); instance.lateUpdateWorkAvailable.Set(); // signal as soon as it can be processed by main thread #endif } #if !DONT_WAIT_FOR_ALL_LATEUPDATE_TASKS - instance.lateUpdateDone[threadIndex].Set(); // signal once after all work is done + instance.lateUpdateDone[taskIndex].Set(); // signal once after all work is done #endif #if SPINE_ENABLE_THREAD_PROFILING instance.profilerSamplerLateUpdate[threadIndex].End(); diff --git a/spine-unity/Assets/Spine/package.json b/spine-unity/Assets/Spine/package.json index 5eab94bcc..233f41559 100644 --- a/spine-unity/Assets/Spine/package.json +++ b/spine-unity/Assets/Spine/package.json @@ -2,7 +2,7 @@ "name": "com.esotericsoftware.spine.spine-unity", "displayName": "spine-unity Runtime", "description": "This plugin provides the spine-unity runtime core and examples. Spine Examples can be installed via the Samples tab.", - "version": "4.3.37", + "version": "4.3.38", "unity": "2018.3", "author": { "name": "Esoteric Software",