[unity] Added automatic load balancing for threading system for improved performance. Closes #3012.

This commit is contained in:
Harald Csaszar 2026-01-19 21:44:44 +01:00
parent 45dbd74fdc
commit 35f0b1c26e
14 changed files with 649 additions and 160 deletions

View File

@ -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**

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
/// <summary>
/// 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.
/// </summary>
public class CircularArray<T> {
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<T> Grow (int b, int t, int newSizePoT) {
CircularArray<T> a = new CircularArray<T>(newSizePoT);
for (int i = t; i < b; i++) {
a.Put(i, this.Get(i));
}
return a;
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 347b87d200483c24aaa3764e9eaf2c8f

View File

@ -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

View File

@ -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;
/// <summary>
/// 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.
/// </summary>
public class LockFreeWorkStealingDeque<T> {
public static readonly T Empty = default(T);
public static readonly T Abort = default(T);
private /*volatile*/ CircularArray<T> 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<T>(capacity);
bottom = 0;
top = 0;
}
/// <summary>Push an element (at the bottom), has to be called by owner of the deque, not a thief.</summary>
public void Push (T item) {
int b = bottom;
int t = top;
CircularArray<T> 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;
}
/// <summary>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.</summary>
public void PushTop (T item) {
int b = bottom;
int t = top;
CircularArray<T> 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;
}
/// <summary>
/// Makes a different worker than the owner steal an element (from the top).
/// Returns false if empty.
/// </summary>
public bool Steal (out T item) {
int t = top;
int b = bottom;
CircularArray<T> 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;
}
/// <summary>Pop an element (from the bottom), has to be called by owner of the deque, not a thief.</summary>
/// <returns>false if empty.</returns>
public bool Pop (out T item) {
int b = bottom;
CircularArray<T> 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;
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ac7c706babeba4847a4a99f0c35ce0fe

View File

@ -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<T> : IDisposable {
public class Task {
public T parameters;
public Action<T, int> function;
}
private readonly int _threadCount;
private readonly Thread[] _threads;
private readonly LockFreeWorkStealingDeque<Task>[] _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<Task>[_threadCount];
_taskAvailable = new AutoResetEvent[_threadCount];
for (int i = 0; i < _threadCount; i++) {
_taskQueues[i] = new LockFreeWorkStealingDeque<Task>(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();
}
}
/// <summary>Enqueues a task item if there is space available, but does
/// not start processing until <see cref="AllowTaskProcessing"/> is called.</summary>
/// <returns>True if the item was successfully enqueued, false otherwise.</returns>
public bool EnqueueTask (int threadIndex, Task task) {
if (threadIndex < 0 || threadIndex >= _threadCount)
throw new ArgumentOutOfRangeException("threadIndex");
_taskQueues[threadIndex].PushTop(task);
return true;
}
/// <summary>
/// Call this method after <see cref="EnqueueTaskWithoutProcessing"/> 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.
/// </summary>
/// <param name="numAsyncThreads">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.</param>
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();
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 14b683fce49a5a641b55e8af49df35a4

View File

@ -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

View File

@ -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<SkeletonUpdateSystem.SkeletonUpdateRange>;
using WorkerPoolTask = LockFreeWorkStealingWorkerPool<SkeletonUpdateSystem.SkeletonUpdateRange>.Task;
#else
using WorkerPool = LockFreeWorkerPool<SkeletonUpdateSystem.SkeletonUpdateRange>;
using WorkerPoolTask = LockFreeWorkerPool<SkeletonUpdateSystem.SkeletonUpdateRange>.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<SkeletonAnimationBase> skeletonAnimationsUpdate = new List<SkeletonAnimationBase>();
public List<SkeletonAnimationBase> skeletonAnimationsFixedUpdate = new List<SkeletonAnimationBase>();
public List<SkeletonAnimationBase> skeletonAnimationsLateUpdate = new List<SkeletonAnimationBase>();
@ -136,14 +169,15 @@ namespace Spine.Unity {
public WorkerPool workerPool;
ExposedList<SkeletonPartitionRange> taskPartitionsUpdate = null;
ExposedList<SkeletonPartitionRange> taskPartitionsLateUpdate = null;
public List<ResetEvent> updateDone = new List<ResetEvent>(4);
public List<ResetEvent> lateUpdateDone = new List<ResetEvent>(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<SkeletonPartitionRange> taskPartitions, out int outAsyncEndExclusive,
int tasksPerThread, int skeletonCount, int numAsyncThreads, int numAvailableThreads) {
int numAsyncTasks = numAsyncThreads * tasksPerThread;
if (taskPartitions == null) {
taskPartitions = new ExposedList<SkeletonPartitionRange>(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<SkeletonAnimationBase> skeletons, UpdateTiming timing,
int numThreads, int numAsyncThreads, int rangePerThread,
int skeletonEnd) {
ExposedList<SkeletonPartitionRange> 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<SkeletonAnimationBase> skeletons, UpdateTiming timing,
int numAsyncThreads, int rangePerThread,
int skeletonEnd, int endIndexThreaded) {
ExposedList<SkeletonPartitionRange> 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<SkeletonPartitionRange> 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();

View File

@ -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",