From 7fa846bb953dfcb8ed87c30d815abe577843359a Mon Sep 17 00:00:00 2001 From: Harald Csaszar Date: Fri, 24 Oct 2025 18:34:43 +0200 Subject: [PATCH] [unity] Threading: Added SkeletonUpdateSystem.Instance.GroupRenderersBySkeletonType and GroupAnimationBySkeletonType properties. Default to disabled. Fixed inconsistent threading skeleton sorting. --- CHANGELOG.md | 1 + .../Threading/SkeletonUpdateSystem.cs | 54 ++++++++++++++----- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db2fe7ab9..f3172c2d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -359,6 +359,7 @@ - Even when threading is enabled, the threading system defaults to `SkeletonRenderer` and `SkeletonAnimation` user callbacks like `UpdateWorld` (not including `AnimationState` callbacks) being issued on the main thread to support existing user code. Can be configured via `SkeletonUpdateSystem.Instance.MainThreadUpdateCallbacks = false` to perform callbacks on worker threads if parallel execution is supported and desired by the user code. `OnPostProcessVertices` is an exception, as it it's deliberately left on worker threads so that parallellization can be utilized. Note that most Unity API calls are restricted to the main thread. - For `SkeletonAnimation.AnimationState` callbacks, there are additional main thread callbacks `MainThreadStart`, `MainThreadInterrupt`, `MainThreadEnd`, `MainThreadDispose`, `MainThreadComplete` and `MainThreadEvent` provided directly at `SkeletonAnimation`, e.g. `SkeletonAnimation.MainThreadComplete` for `SkeletonAnimation.AnimationState.Complete` and so on. Please note that this requires a change of user code to subscribe to these main thread delegate variants instead. - The same applies to the `TrackEntry.Start`, `Interrupt`, `End`, `Dispose`, `Complete`, and `Event` events. If you need these callbacks to run on the main thread instead of worker threads, you should register them using the corresponding `SkeletonAnimation.MainThreadStart`, `MainThreadInterrupt`, etc. callbacks. Note that this does require a small code change, since these events are **not** automatically unregistered when the `TrackEntry` is removed. You’ll need to handle that manually, typically with logic such as `if (trackEntry.Animation == attackAnimation) ..`. + - 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. - **Deprecated** diff --git a/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/SkeletonUpdateSystem.cs b/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/SkeletonUpdateSystem.cs index a158ed8c4..5e5863f14 100644 --- a/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/SkeletonUpdateSystem.cs +++ b/spine-unity/Assets/Spine/Runtime/spine-unity/Threading/SkeletonUpdateSystem.cs @@ -106,18 +106,18 @@ namespace Spine.Unity { } public static int SkeletonSortComparer (ISkeletonRenderer first, ISkeletonRenderer second) { - Skeleton firstSkeleton = first.Skeleton; - Skeleton secondSkeleton = second.Skeleton; - if (firstSkeleton == null) return secondSkeleton == null ? 0 : -1; - else if (secondSkeleton == null) return 1; - else return firstSkeleton.Data.GetHashCode() - secondSkeleton.Data.GetHashCode(); + SkeletonDataAsset firstDataAsset = first.SkeletonDataAsset; + SkeletonDataAsset secondDataAsset = second.SkeletonDataAsset; + if (firstDataAsset == null) return secondDataAsset == null ? 0 : -1; + else if (secondDataAsset == null) return 1; + else return firstDataAsset.GetHashCode() - secondDataAsset.GetHashCode(); } public static int SkeletonSortComparer (SkeletonAnimationBase first, SkeletonAnimationBase second) { - Skeleton firstSkeleton = first.Skeleton; - Skeleton secondSkeleton = second.Skeleton; - if (firstSkeleton == null) return secondSkeleton == null ? 0 : -1; - else if (secondSkeleton == null) return 1; - else return firstSkeleton.Data.GetHashCode() - secondSkeleton.Data.GetHashCode(); + SkeletonDataAsset firstDataAsset = first.SkeletonDataAsset; + SkeletonDataAsset secondDataAsset = second.SkeletonDataAsset; + if (firstDataAsset == null) return secondDataAsset == null ? 0 : -1; + else if (secondDataAsset == null) return 1; + else return firstDataAsset.GetHashCode() - secondDataAsset.GetHashCode(); } public static readonly Comparison SkeletonRendererComparer = SkeletonSortComparer; public static readonly Comparison SkeletonAnimationComparer = SkeletonSortComparer; @@ -162,6 +162,9 @@ namespace Spine.Unity { protected bool mainThreadUpdateCallbacks = true; protected CoroutineIterator[] splitUpdateMethod = null; + protected bool sortSkeletonRenderers = false; + protected bool sortSkeletonAnimations = false; + int UsedThreadCount { get { if (usedThreadCount < 0) { @@ -184,6 +187,26 @@ namespace Spine.Unity { get { return mainThreadUpdateCallbacks; } } + /// + /// Optimization setting. Enable to group ISkeletonRenderers by type (by SkeletonDataAsset) for mesh updates. + /// Potentially allows for better cache locality, however this may be detrimental if skeleton types vary in + /// complexity. + /// + public bool GroupRenderersBySkeletonType { + set { sortSkeletonRenderers = value; } + get { return sortSkeletonRenderers; } + } + + /// + /// Optimization setting. Enable to group skeletons to be animated by type (by SkeletonDataAsset). + /// Potentially allows for better cache locality, however this may be detrimental if skeleton types vary in + /// complexity. + /// + public bool GroupAnimationBySkeletonType { + set { sortSkeletonAnimations = value; } + get { return sortSkeletonAnimations; } + } + #if USE_THREADED_ANIMATION_UPDATE public void RegisterForUpdate (UpdateTiming updateTiming, SkeletonAnimationBase skeletonAnimation) { skeletonAnimation.isUpdatedExternally = true; @@ -240,8 +263,10 @@ namespace Spine.Unity { public void UpdateAsync (List skeletons, UpdateTiming updateTiming) { if (skeletons.Count == 0) return; - // sort by skeleton data to allow for better cache utilization. - skeletons.Sort(SkeletonAnimationComparer); + + // Sort by skeleton data to allow for better cache utilization. + if (sortSkeletonAnimations) + skeletons.Sort(SkeletonAnimationComparer); int numThreads = UsedThreadCount; #if RUN_ALL_ON_MAIN_THREAD @@ -425,8 +450,9 @@ namespace Spine.Unity { public void LateUpdateAsync () { if (skeletonRenderers.Count == 0) return; - // sort by skeleton data to allow for better cache utilization. - skeletonRenderers.Sort(SkeletonRendererComparer); + // Sort by skeleton data to allow for better cache utilization. + if (sortSkeletonRenderers) + skeletonRenderers.Sort(SkeletonRendererComparer); int numThreads = UsedThreadCount; #if RUN_ALL_ON_MAIN_THREAD