mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-02-04 14:24:53 +08:00
[unity] Added automatic load balancing for threading system for improved performance. Closes #3012.
This commit is contained in:
parent
45dbd74fdc
commit
35f0b1c26e
@ -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**
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 347b87d200483c24aaa3764e9eaf2c8f
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ac7c706babeba4847a4a99f0c35ce0fe
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 14b683fce49a5a641b55e8af49df35a4
|
||||
@ -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
|
||||
|
||||
@ -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,15 +323,17 @@ 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) {
|
||||
for (int t = updateDone.Count; t < numTasks; ++t) {
|
||||
updateDone.Add(new ResetEvent(false));
|
||||
}
|
||||
}
|
||||
|
||||
if (exceptions == null) {
|
||||
exceptions = new Exception[numThreads];
|
||||
@ -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 UpdateAsyncThreadedCallbacks (List<SkeletonAnimationBase> skeletons, UpdateTiming timing,
|
||||
int numThreads, int numAsyncThreads, int rangePerThread,
|
||||
int skeletonEnd) {
|
||||
protected void PartitionTasks(ref ExposedList<SkeletonPartitionRange> taskPartitions, out int outAsyncEndExclusive,
|
||||
int tasksPerThread, int skeletonCount, int numAsyncThreads, int numAvailableThreads) {
|
||||
|
||||
int start = 0;
|
||||
int end = Mathf.Min(rangePerThread, skeletonEnd);
|
||||
int numAsyncTasks = numAsyncThreads * tasksPerThread;
|
||||
if (taskPartitions == null) {
|
||||
taskPartitions = new ExposedList<SkeletonPartitionRange>(numAsyncTasks);
|
||||
}
|
||||
if (taskPartitions.Count != numAsyncTasks) {
|
||||
taskPartitions.Resize(numAsyncTasks);
|
||||
}
|
||||
|
||||
for (int t = 0; t < numThreads; ++t) {
|
||||
var range = new SkeletonUpdateRange() {
|
||||
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,
|
||||
ExposedList<SkeletonPartitionRange> asyncTaskPartitions, int numAsyncThreads, int skeletonEnd) {
|
||||
|
||||
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 = partition.rangeStart,
|
||||
rangeEndExclusive = partition.rangeEndExclusive,
|
||||
taskIndex = taskIndex,
|
||||
frameCount = Time.frameCount,
|
||||
updateTiming = timing
|
||||
};
|
||||
|
||||
if (t < numAsyncThreads) {
|
||||
UpdateSkeletonsAsync(range, t);
|
||||
} else {
|
||||
UpdateSkeletonsAsync(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 = timing
|
||||
};
|
||||
UpdateSkeletonsSynchronous(skeletons, range);
|
||||
}
|
||||
|
||||
start = end;
|
||||
if (start >= skeletonEnd) {
|
||||
while (++t < numAsyncThreads)
|
||||
updateDone[t].Set();
|
||||
break;
|
||||
}
|
||||
end = Mathf.Min(end + rangePerThread, skeletonEnd);
|
||||
}
|
||||
|
||||
WaitForThreadUpdateTasks(numAsyncThreads);
|
||||
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) {
|
||||
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
|
||||
};
|
||||
|
||||
UpdateSkeletonsAsyncSplit(range, t);
|
||||
|
||||
start = end;
|
||||
if (start >= skeletonEnd) {
|
||||
while (++t < numAsyncThreads) {
|
||||
updateDone[t].Set();
|
||||
UpdateSkeletonsAsyncSplit(range, partition.threadIndex);
|
||||
}
|
||||
break;
|
||||
}
|
||||
end = Mathf.Min(end + rangePerThread, skeletonEnd);
|
||||
}
|
||||
|
||||
#if ENABLE_WORK_STEALING
|
||||
workerPool.AllowTaskProcessing(numAsyncThreads);
|
||||
#endif
|
||||
// main thread
|
||||
if (isFirstIteration && start != skeletonEnd) {
|
||||
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 = start,
|
||||
rangeEndExclusive = end,
|
||||
rangeStart = lastAsyncPartition.rangeEndExclusive,
|
||||
rangeEndExclusive = skeletonEnd,
|
||||
taskIndex = -1,
|
||||
frameCount = Time.frameCount,
|
||||
updateTiming = timing
|
||||
};
|
||||
// this main thread does complete update work in the first iteration, otherwise it's only waiting.
|
||||
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,98 +552,110 @@ 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) {
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
|
||||
start = end;
|
||||
if (start >= skeletonEnd) {
|
||||
while (++t < numAsyncThreads)
|
||||
lateUpdateDone[t].Set();
|
||||
break;
|
||||
}
|
||||
end = Mathf.Min(end + rangePerThread, skeletonEnd);
|
||||
}
|
||||
|
||||
#if RUN_ALL_ON_MAIN_THREAD
|
||||
return; // nothing left to do after all processed as LateUpdateSkeletonsSynchronous
|
||||
#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,7 +777,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();
|
||||
#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();
|
||||
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user