Skip to content

Commit 35f0b1c

Browse files
committed
[unity] Added automatic load balancing for threading system for improved performance. Closes #3012.
1 parent 45dbd74 commit 35f0b1c

File tree

14 files changed

+644
-155
lines changed

14 files changed

+644
-155
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,8 @@
364364
- 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.
365365
- Added previously missing editor drag & drop skeleton instantiation option *SkeletonGraphic (UI) Mecanim* combining components `SkeletonGraphic` and `SkeletonMecanim`.
366366
- 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.
367+
- 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.
368+
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.
367369

368370
- **Deprecated**
369371

spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/BuildSettings.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ public static class SpineBuildEnvUtility {
8686
public const string SPINE_ALLOW_UNSAFE_CODE = "SPINE_ALLOW_UNSAFE";
8787
public const string SPINE_AUTO_UPGRADE_COMPONENTS_OFF = "SPINE_AUTO_UPGRADE_COMPONENTS_OFF";
8888
public const string SPINE_ENABLE_THREAD_PROFILING = "SPINE_ENABLE_THREAD_PROFILING";
89+
public const string SPINE_DISABLE_LOAD_BALANCING = "SPINE_DISABLE_LOAD_BALANCING";
8990

9091
static bool IsInvalidGroup (BuildTargetGroup group) {
9192
int gi = (int)group;

spine-unity/Assets/Spine/Editor/spine-unity/Editor/Utility/Preferences.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,18 @@ public static void HandlePreferencesGUI () {
512512
() => RuntimeSettings.UseThreadedAnimation, value => RuntimeSettings.UseThreadedAnimation = value,
513513
new GUIContent("Threaded Animation", "Global setting for the equally named SkeletonAnimation and SkeletonGraphic Inspector parameter."));
514514

515+
#if SPINE_DISABLE_LOAD_BALANCING
516+
bool loadBalancingEnabled = false;
517+
#else
518+
bool loadBalancingEnabled = true;
519+
#endif
520+
using (new GUILayout.HorizontalScope()) {
521+
EditorGUILayout.PrefixLabel(new GUIContent("Load Balancing",
522+
"Enable load balancing to better utilize threads." +
523+
"Only has an effect when using threaded animation or threaded mesh generation."));
524+
EnableDisableDefineButtons(SpineBuildEnvUtility.SPINE_DISABLE_LOAD_BALANCING, loadBalancingEnabled, invert: true);
525+
}
526+
515527
#if ALLOWS_CUSTOM_PROFILING
516528
#if SPINE_ENABLE_THREAD_PROFILING
517529
bool threadProfilingEnabled = true;

spine-unity/Assets/Spine/Editor/spine-unity/Editor/Windows/SpinePreferences.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,19 @@ public static void HandlePreferencesGUI (SerializedObject settings) {
522522
() => RuntimeSettings.UseThreadedAnimation, value => RuntimeSettings.UseThreadedAnimation = value,
523523
new GUIContent("Threaded Animation", "Global setting for the equally named SkeletonAnimation and SkeletonGraphic Inspector parameter."));
524524

525+
#if SPINE_DISABLE_LOAD_BALANCING
526+
bool loadBalancingEnabled = false;
527+
#else
528+
bool loadBalancingEnabled = true;
529+
#endif
530+
using (new GUILayout.HorizontalScope()) {
531+
EditorGUILayout.PrefixLabel(new GUIContent("Load Balancing",
532+
"Enable load balancing to better utilize threads." +
533+
"Only has an effect when using threaded animation or threaded mesh generation."));
534+
SpineEditorUtilities.EnableDisableDefineButtons(SpineBuildEnvUtility.SPINE_DISABLE_LOAD_BALANCING,
535+
loadBalancingEnabled, invert: true);
536+
}
537+
525538
#if ALLOWS_CUSTOM_PROFILING
526539
#if SPINE_ENABLE_THREAD_PROFILING
527540
bool threadProfilingEnabled = true;
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/******************************************************************************
2+
* Spine Runtimes License Agreement
3+
* Last updated July 28, 2023. Replaces all prior versions.
4+
*
5+
* Copyright (c) 2013-2025, Esoteric Software LLC
6+
*
7+
* Integration of the Spine Runtimes into software or otherwise creating
8+
* derivative works of the Spine Runtimes is permitted under the terms and
9+
* conditions of Section 2 of the Spine Editor License Agreement:
10+
* http://esotericsoftware.com/spine-editor-license
11+
*
12+
* Otherwise, it is permitted to integrate the Spine Runtimes into software or
13+
* otherwise create derivative works of the Spine Runtimes (collectively,
14+
* "Products"), provided that each user of the Products must obtain their own
15+
* Spine Editor license and redistribution of the Products in any form must
16+
* include this license and copyright notice.
17+
*
18+
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
19+
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21+
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
22+
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23+
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
24+
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
25+
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
27+
* SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28+
*****************************************************************************/
29+
30+
using System;
31+
using System.Threading;
32+
33+
/// <summary>
34+
/// An array with wrap-around access for LockFreeWorkStealingDeque based on the paper
35+
/// "Dynamic Circular Work-Stealing Deque", authors David Chase and Yossi Lev.
36+
/// https://www.dre.vanderbilt.edu/~schmidt/PDF/work-stealing-dequeue.pdf
37+
/// Modified to support negative indices.
38+
/// </summary>
39+
public class CircularArray<T> {
40+
private int size;
41+
private T[] segment;
42+
private uint mask;
43+
44+
public int Size { get { return size; } }
45+
46+
public CircularArray (int sizePoT) {
47+
this.size = sizePoT;
48+
segment = new T[sizePoT];
49+
mask = (uint)(sizePoT - 1);
50+
}
51+
52+
public T Get (int i) {
53+
return this.segment[((uint)i) & mask];
54+
}
55+
56+
public void Put (int i, T item) {
57+
this.segment[((uint)i) & mask] = item;
58+
}
59+
60+
public CircularArray<T> Grow (int b, int t, int newSizePoT) {
61+
CircularArray<T> a = new CircularArray<T>(newSizePoT);
62+
for (int i = t; i < b; i++) {
63+
a.Put(i, this.Get(i));
64+
}
65+
return a;
66+
}
67+
}

spine-unity/Assets/Spine/Runtime/spine-unity/Threading/CircularArray.cs.meta

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

spine-unity/Assets/Spine/Runtime/spine-unity/Threading/LockFreeSPSCQueue.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Spine Runtimes License Agreement
33
* Last updated July 28, 2023. Replaces all prior versions.
44
*
5-
* Copyright (c) 2013-2024, Esoteric Software LLC
5+
* Copyright (c) 2013-2025, Esoteric Software LLC
66
*
77
* Integration of the Spine Runtimes into software or otherwise creating
88
* derivative works of the Spine Runtimes is permitted under the terms and
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/******************************************************************************
2+
* Spine Runtimes License Agreement
3+
* Last updated July 28, 2023. Replaces all prior versions.
4+
*
5+
* Copyright (c) 2013-2025, Esoteric Software LLC
6+
*
7+
* Integration of the Spine Runtimes into software or otherwise creating
8+
* derivative works of the Spine Runtimes is permitted under the terms and
9+
* conditions of Section 2 of the Spine Editor License Agreement:
10+
* http://esotericsoftware.com/spine-editor-license
11+
*
12+
* Otherwise, it is permitted to integrate the Spine Runtimes into software or
13+
* otherwise create derivative works of the Spine Runtimes (collectively,
14+
* "Products"), provided that each user of the Products must obtain their own
15+
* Spine Editor license and redistribution of the Products in any form must
16+
* include this license and copyright notice.
17+
*
18+
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
19+
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21+
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
22+
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23+
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
24+
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
25+
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE
27+
* SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28+
*****************************************************************************/
29+
30+
using System.Threading;
31+
32+
/// <summary>
33+
/// A generic lock-free deque supporting work-stealing based on the paper
34+
/// "Dynamic Circular Work-Stealing Deque", authors David Chase and Yossi Lev
35+
/// https://www.dre.vanderbilt.edu/~schmidt/PDF/work-stealing-dequeue.pdf.
36+
/// Requires that Push and Pop are called from the same thread.
37+
/// Simplified by not supporting growing the array size during a Push,
38+
/// in our usage scenario we populate tasks ahead of time before the first Pop.
39+
/// </summary>
40+
public class LockFreeWorkStealingDeque<T> {
41+
public static readonly T Empty = default(T);
42+
public static readonly T Abort = default(T);
43+
44+
private /*volatile*/ CircularArray<T> activeArray;
45+
private volatile int bottom = 0;
46+
private volatile int top = 0;
47+
48+
public int Capacity { get { return activeArray.Size; } }
49+
50+
public LockFreeWorkStealingDeque (int capacity) {
51+
capacity = UnityEngine.Mathf.NextPowerOfTwo(capacity);
52+
activeArray = new CircularArray<T>(capacity);
53+
bottom = 0;
54+
top = 0;
55+
}
56+
57+
/// <summary>Push an element (at the bottom), has to be called by owner of the deque, not a thief.</summary>
58+
public void Push (T item) {
59+
int b = bottom;
60+
int t = top;
61+
CircularArray<T> a = this.activeArray;
62+
int size = b - t;
63+
if (size >= a.Size - 1) {
64+
a = a.Grow(b, t, a.Size * 2);
65+
this.activeArray = a;
66+
}
67+
a.Put(b, item);
68+
bottom = b + 1;
69+
}
70+
71+
/// <summary>Non-standard addition for ahead-of-time pushing to maintain queue FIFO order.
72+
/// Push an element at the top, must only be called before any other thread calls Push, Pop or Steal.</summary>
73+
public void PushTop (T item) {
74+
int b = bottom;
75+
int t = top;
76+
CircularArray<T> a = this.activeArray;
77+
int size = b - t;
78+
if (size >= a.Size - 1) {
79+
a = a.Grow(b, t, a.Size * 2);
80+
this.activeArray = a;
81+
}
82+
int newT = t - 1;
83+
a.Put(newT, item);
84+
top = newT;
85+
}
86+
87+
/// <summary>
88+
/// Makes a different worker than the owner steal an element (from the top).
89+
/// Returns false if empty.
90+
/// </summary>
91+
public bool Steal (out T item) {
92+
int t = top;
93+
int b = bottom;
94+
CircularArray<T> a = this.activeArray;
95+
int size = b - t;
96+
if (size <= 0) {
97+
item = Empty;
98+
return false;
99+
}
100+
T o = a.Get(t);
101+
// increment top
102+
if (Interlocked.CompareExchange(ref top, t + 1, t) != t) {
103+
item = Abort;
104+
return false;
105+
}
106+
item = o;
107+
return true;
108+
}
109+
110+
/// <summary>Pop an element (from the bottom), has to be called by owner of the deque, not a thief.</summary>
111+
/// <returns>false if empty.</returns>
112+
public bool Pop (out T item) {
113+
int b = bottom;
114+
CircularArray<T> a = this.activeArray;
115+
--b;
116+
this.bottom = b;
117+
int t = top;
118+
int size = b - t;
119+
if (size < 0) {
120+
bottom = t;
121+
item = Empty;
122+
return false;
123+
}
124+
T o = a.Get(b);
125+
if (size > 0) {
126+
item = o;
127+
return true;
128+
}
129+
130+
bool wasSuccessful = true;
131+
if (Interlocked.CompareExchange(ref top, t + 1, t) != t) {
132+
item = Empty;
133+
wasSuccessful = false;
134+
}
135+
else {
136+
item = o;
137+
}
138+
bottom = t + 1;
139+
return wasSuccessful;
140+
}
141+
}

spine-unity/Assets/Spine/Runtime/spine-unity/Threading/LockFreeWorkStealingDeque.cs.meta

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)