Initial commit: Client Doc docs Server Tools
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
/******************************************************************************
|
||||
* Spine Runtimes License Agreement
|
||||
* Last updated July 28, 2023. Replaces all prior versions.
|
||||
*
|
||||
* Copyright (c) 2013-2026, 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,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 347b87d200483c24aaa3764e9eaf2c8f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,80 @@
|
||||
/******************************************************************************
|
||||
* Spine Runtimes License Agreement
|
||||
* Last updated July 28, 2023. Replaces all prior versions.
|
||||
*
|
||||
* Copyright (c) 2013-2026, 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>
|
||||
/// A generic lock-free single-producer single-consumer queue.
|
||||
/// </summary>
|
||||
public class LockFreeSPSCQueue<T> {
|
||||
private readonly int capacity;
|
||||
private readonly T[] circularBuffer;
|
||||
private int headIndex;
|
||||
private int tailIndex;
|
||||
|
||||
public LockFreeSPSCQueue (int allocatedCapacity) {
|
||||
capacity = allocatedCapacity;
|
||||
circularBuffer = new T[allocatedCapacity];
|
||||
headIndex = tailIndex = 0;
|
||||
}
|
||||
|
||||
/// <summary>Enqueues an item if there is space available.</summary>
|
||||
/// <returns>True if the item was successfully enqueued, false otherwise.</returns>
|
||||
public bool Enqueue (T item) {
|
||||
int head = Thread.VolatileRead(ref headIndex);
|
||||
int nextHead = (head + 1) % capacity;
|
||||
|
||||
if (nextHead == Thread.VolatileRead(ref tailIndex))
|
||||
return false; // queue is full
|
||||
|
||||
circularBuffer[head] = item;
|
||||
Thread.VolatileWrite(ref headIndex, nextHead);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dequeues an item unless the queue is empty.
|
||||
/// </summary>
|
||||
/// <param name="item">The dequeued item, or the default element if empty.</param>
|
||||
/// <returns>True if the item was successfully dequeued, false otherwise.</returns>
|
||||
public bool Dequeue (out T item) {
|
||||
int tail = Thread.VolatileRead(ref tailIndex);
|
||||
|
||||
if (tail == Thread.VolatileRead(ref headIndex)) {
|
||||
item = default(T); // queue is empty
|
||||
return false;
|
||||
}
|
||||
|
||||
item = circularBuffer[tail];
|
||||
int nextTail = (tail + 1) % capacity;
|
||||
Thread.VolatileWrite(ref tailIndex, nextTail);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e6dd6c3762358e442b1b446b610e959e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,141 @@
|
||||
/******************************************************************************
|
||||
* Spine Runtimes License Agreement
|
||||
* Last updated July 28, 2023. Replaces all prior versions.
|
||||
*
|
||||
* Copyright (c) 2013-2026, 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,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ac7c706babeba4847a4a99f0c35ce0fe
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,143 @@
|
||||
/******************************************************************************
|
||||
* Spine Runtimes License Agreement
|
||||
* Last updated July 28, 2023. Replaces all prior versions.
|
||||
*
|
||||
* Copyright (c) 2013-2026, 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 14b683fce49a5a641b55e8af49df35a4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,113 @@
|
||||
/******************************************************************************
|
||||
* Spine Runtimes License Agreement
|
||||
* Last updated July 28, 2023. Replaces all prior versions.
|
||||
*
|
||||
* Copyright (c) 2013-2026, 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.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 LockFreeWorkerPool<T> : IDisposable {
|
||||
public class Task {
|
||||
public T parameters;
|
||||
public Action<T, int> function;
|
||||
}
|
||||
|
||||
private readonly int _threadCount;
|
||||
private readonly Thread[] _threads;
|
||||
private readonly LockFreeSPSCQueue<Task>[] _taskQueues;
|
||||
private readonly AutoResetEvent[] _taskAvailable;
|
||||
private volatile bool _running = true;
|
||||
|
||||
public LockFreeWorkerPool (int threadCount, int queueCapacity = 2) {
|
||||
_threadCount = threadCount;
|
||||
_threads = new Thread[_threadCount];
|
||||
_taskQueues = new LockFreeSPSCQueue<Task>[_threadCount];
|
||||
_taskAvailable = new AutoResetEvent[_threadCount];
|
||||
|
||||
for (int i = 0; i < _threadCount; i++) {
|
||||
_taskQueues[i] = new LockFreeSPSCQueue<Task>(queueCapacity);
|
||||
_taskAvailable[i] = new AutoResetEvent(false);
|
||||
|
||||
int index = i; // Capture the index for the thread
|
||||
_threads[i] = new Thread(() => WorkerLoop(index));
|
||||
_threads[i].Start();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Enqueues a task item if there is space available.</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");
|
||||
|
||||
bool success = _taskQueues[threadIndex].Enqueue(task);
|
||||
if (!success) {
|
||||
return false;
|
||||
}
|
||||
|
||||
_taskAvailable[threadIndex].Set();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void WorkerLoop (int threadIndex) {
|
||||
#if SPINE_ENABLE_THREAD_PROFILING
|
||||
Profiler.BeginThreadProfiling("Spine Threads", "Spine Thread " + threadIndex);
|
||||
#endif
|
||||
while (_running) {
|
||||
Task task = null;
|
||||
bool success = _taskQueues[threadIndex].Dequeue(out task);
|
||||
if (success) {
|
||||
task.function(task.parameters, threadIndex);
|
||||
} else {
|
||||
_taskAvailable[threadIndex].WaitOne();
|
||||
}
|
||||
}
|
||||
#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,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 013e48636028fc245a5581519017ff3e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9a1427d268719124ab5d9841d5f4ddd7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user