• Unity
  • Delegate and AOT compilation for XBox One

While trying to port our project to the XBox One platform, our game would always crash at the following line in SkeletonUtility.cs at around line 164:

skeletonRenderer.OnRebuild -= HandleRendererReset;
skeletonRenderer.OnRebuild += HandleRendererReset;

if (skeletonAnimation != null) {
skeletonAnimation.UpdateLocal -= UpdateLocal;
skeletonAnimation.UpdateLocal += UpdateLocal; /// crashes at this line
}

After some googling, it seems there's some issues with event delegate in ahead-of-time compile.
Any suggestion on how to fix this?

Related Discussions
...

Some dev shared a fix for this. It was some attribute. Let me look for it.


20 Sep 2016 8:22 am


[Unity] Fixed event syntax cause JIT compiling on iOS devices by yKimisaki · Pull Request #403 · EsotericSoftware/spine-runtimes · GitHub
This seems to show the pattern.

Change:

//      public event StartEndDelegate Start; // old
      private StartEndDelegate start;
      public event StartEndDelegate Start
      {
         [MethodImpl(MethodImplOptions.Synchronized)]
         add { start += value; }

     [MethodImpl(MethodImplOptions.Synchronized)]
     remove { start -= value; }
  }

Then, optionally, change all internal calls to use the private delegate.


20 Sep 2016 8:23 am


These changes may be applied soon but we'd like to make sure it doesn't inadvertently break stuff for everyone else.

Just tried the fix and it's still crashing at the same line.
How does '[MethodImpl(MethodImplOptions.Synchronized)]' fix the AOT compile error? It seems all it does is some sort of synchronize lock.

That worked for that other guy.
The heart of it seems to have been that if you leave events mostly to the compiler, its magical implementation under the hood tries to call System.Threading.Interlocked.CompareExchange<T>(ref T, T, T) bydefault. And the known issue is that some generic methods causes problems with AOT targets.

Though particularly, that other person was using the fix for iOS and precompiled DLL.

So you googled and there was no hint on how events should be implemented to work on AOT target platforms?

You'll probably have better luck getting an answer to this on the Unity forums. AOT compilation via IL2CPP is still a bit wonky, and we aren't experts on it I'm afraid.

Solved the problem by replacing the delegate 'UpdateBoneDelegate' with another interface structure called 'ISkeletonAnimationCaller'. Changed the ISkeletonAnimation to use the other interface instead of the original delegate.

Thanks for sharing!
That's an odd solution. What's the logic behind it?

Sounds harmless enough to apply immediately to the official runtime.

Yikes this was something I had never considered. I just ditched protobuf because it is not as easy to work with as json for save game data and isn't AOT friendly. We are pretty far from release still, but I'd like to know if spine/unity will be xbone/ps4 friendly and when.

Pharan wrote

Thanks for sharing!
That's an odd solution. What's the logic behind it?

Sounds harmless enough to apply immediately to the official runtime.

Hey Pharan,
After some more digging in some non-spine-related area of our code, we found using interface actually crashes our game(which is what our solution is based upon). Although the exact reason is still unknown. We feel there's something else going on in Unity's xbox compiler so it's not necessarily a problem of AOT's. But the following changes in spine did get us further, or made our game crash a few hundred lines later.

In essence, the change is made to avoid the delegate structure inside an interface, by having three other classes implement a new interface. In details: (If you want the source changes, we can just send you the files that are changed.)

// in ISkeletonAnimation.cs
// event UpdateBonesDelegate UpdateLocal;
void addLocalCaller(ISkeletonAnimationCaller caller);
void removeLocalCaller(ISkeletonAnimationCaller caller);
// and the same goes for UpdateWorld and UpdateComplete

// in SkeletonAnimation.cs
// protected event UpdateBonesDelegate _UpdateLocal;
List<ISkeletonAnimationCaller> localCallerList;
public void addLocalCaller(ISkeletonAnimationCaller caller){
if (localCallerList == null)
   localCallerList = new List<ISkeletonAnimationCaller> ();
   localCallerList.Add (caller);
}
public void removeLocalCaller(ISkeletonAnimationCaller caller){
   if (localCallerList == null)
      return;
   localCallerList.Remove (caller);
}
// the same goes for 'worldCallerList' and 'completeCallerList'

// in SkeletonAnimation.cs::Update() we commented some code and replace it with the following:
 if (localCallerList != null) 
      for (int i = 0; i < localCallerList.Count; i++) 
           localCallerList [i].onUpdateLocal (this);

  skeleton.UpdateWorldTransform();

if (worldCallerList != null) { 
      for (int i = 0; i < worldCallerList.Count; i++) 
           worldCallerList [i].onUpdateWorld (this);
      skeleton.UpdateWorldTransform();
   }
   if (completeCallerList != null)
      for (int i = 0; i < completeCallerList.Count; i++)
            completeCallerList [i].onUpdateComplete (this);

// and the same treatment is done on SkeletonAnimator.cs

// we define a new interface ISkeletonAnimationCaller as following:
public interface ISkeletonAnimationCaller {      
   void onUpdateLocal(ISkeletonAnimation val);
   void onUpdateWorld(ISkeletonAnimation val);
   void onUpdateComplete(ISkeletonAnimation val);
}

// SkeletonRagdoll.cs, SkeletonRagdoll2D.cs and SkeletonUtility.cs implement such interface.
// add the following interface implementation methods in SkeletonRagdoll.cs and SkeletonRagdoll2D.cs
public void onUpdateWorld(ISkeletonAnimation val){
   UpdateSpineSkeleton (val);
}
public void onUpdateComplete(ISkeletonAnimation val){
}
public void onUpdateLocal(ISkeletonAnimation val){
}

// add the following interface implementation methods in SkeletonUtility.cs
public void onUpdateLocal(ISkeletonAnimation val){
   UpdateLocal (val);
}
public void onUpdateWorld(ISkeletonAnimation val){
   UpdateWorld (val);
}
public void onUpdateComplete(ISkeletonAnimation val){
   UpdateComplete (val);
}

Oh, so you fixed it by doing events the java way.
Probably not a good idea for vanilla spine-csharp but good to know.