Search Unity

Supporting [ExecuteAlways]/[ExecuteInEditMode] for NativeArray

Discussion in 'Entity Component System' started by Vanamerax, Jul 11, 2020.

  1. Vanamerax

    Vanamerax

    Joined:
    Jan 12, 2012
    Posts:
    938
    Hi everyone!

    I have been searching around for a solution for this problem, but there does not appear to be much information about this online so here it goes:


    I have a monobehaviour that stores data in a NativeArray such that it can be used in the job system. This works fine if the script only runs in play mode. However, now I want to enable the script in edit mode using the ExecuteAlways or ExecuteInEditMode attributes. See the following example class:
    Code (CSharp):
    1.  
    2. [ExecuteAlways]
    3. public class Tets : MonoBehaviour, ISerializationCallbackReceiver
    4. {
    5.     //This data will be used in-game but must also be present in edit mode after deserialization.
    6.     private NativeArray<byte> data;
    7.  
    8.     //This is only used for serialization in editor
    9.     [SerializeField]
    10.     private byte[] serializedData;
    11.  
    12.     public void OnAfterDeserialize()
    13.     {
    14.         serializedData = data.ToArray();
    15.  
    16.         //Cannot do this here because this function will be called twice and will override serializedData with an empty array!!
    17.         //data.Dispose();
    18.     }
    19.  
    20.     public void OnBeforeSerialize()
    21.     {
    22.         data = new NativeArray<byte>(serializedData, Allocator.Persistent);
    23.     }
    24. }
    Now comes the problem: the NativeArray is not serialized property by default in Unity. I tried to remedy this by using the ISerializationCallbackReceiver and copying over the NativeArray contents to a managed array and move is back during deserialization.

    Now I need to Dispose the NativeArray on the right time, but this doesn't seem to be as straight forward, because:
    According to the diagram this thread, OnBeforeSerialize is called twice when domain reloading is not disabled. So I cannot dispose the NativeArray here since serialization will break for the second time OnBeforeSerialize is called. The obvious solution would be to dispose the NativeArray during OnAfterDeserialization instead. HOWEVER, doing this leads to the 'A native collection has not been disposed' error when entering playmode, because apparently I must have disposed the NativeArray before the OnBeforeSerialize callback is going to be called (and I assume that is, before AppDomain.Unload is called).
    upload_2020-7-11_14-55-25.png

    So my question to you, and ESPECIALLY to the Unity staff is, how on earth am I supposed to handle this scenario and properly serialize/deserialize NativeArrays such that I can make use of the ExecuteAlways/ExecuteInEditMode attributes?? @alexeyzakharov , @Joachim_Ante

    Any help is greatly appreciated.
     
    alexeyzakharov likes this.
  2. AnthonyReddan

    AnthonyReddan

    Joined:
    Feb 27, 2018
    Posts:
    39
    alexeyzakharov likes this.
  3. Vanamerax

    Vanamerax

    Joined:
    Jan 12, 2012
    Posts:
    938
    Thank you, this worked to get the data to stay in edit mode. However I am still wondering how this work when serializing/saving the scene or prefab. When do I need to store my data to the managed array if I want to save the scene?
     
  4. AnthonyReddan

    AnthonyReddan

    Joined:
    Feb 27, 2018
    Posts:
    39
    Oh right, I totally misunderstood your needs here. I would've approached the problem the same way as you. Except maybe try this?

    Code (CSharp):
    1.     public void OnAfterDeserialize()
    2.     {
    3.         if (data.IsCreated)
    4.         {
    5.             serializedData = data.ToArray();
    6.             data.Dispose();
    7.         }
    8.     }
    9.  
    10.     public void OnBeforeSerialize()
    11.     {
    12.         if (!data.IsCreated)
    13.             data = new NativeArray<byte>(serializedData, Allocator.Persistent);
    14.     }
     
  5. amarcolina

    amarcolina

    Joined:
    Jun 19, 2014
    Posts:
    65
    I think you have your logic backwards? After deserialization you want to move the just-deserialized data into your NativeArray, not the other way around. And right before you serialize data you want to store the data _into_ the array that is about to be serialized.
     
    Last edited: Jul 14, 2020
    AnthonyReddan likes this.
  6. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    I think the better approach is probably keep your native containers in a system let it manage their lifecycle. Then make calls into that system to do something with the container.

    You can call DefaultWorldInitialization.DefaultLazyEditModeInitialize() in edit mode to ensure that the default world is created before accessing systems.
     
    alexeyzakharov likes this.
  7. alexeyzakharov

    alexeyzakharov

    Joined:
    Jul 2, 2014
    Posts:
    507
    Hi!
    Alternatively I would try to dispose NativeArray in a finalizer. Something like:
    Code (CSharp):
    1. ~Tets()
    2. {
    3.    data.Dispose();
    4. }
    I would treat NativeArray as an unmanaged resource and lifecycle similar to IDisposable pattern.
    The dispose check happens after domain is unloaded and finalizer is one of the last pieces that get called during domain reload.

    Plus what already was said above.
     
  8. AnthonyReddan

    AnthonyReddan

    Joined:
    Feb 27, 2018
    Posts:
    39
    Will implementing a Finalizer also resolve some of the issues I outline here? I'm guessing maybe not because of the ScriptedImporter interaction?
     
    alexeyzakharov likes this.
  9. alexeyzakharov

    alexeyzakharov

    Joined:
    Jul 2, 2014
    Posts:
    507
    I would expect so - disposing NativeArray in a finalizer would prolong its lifecycle management to a lifecycle of a managed shell of UnityEngine.Object. Then if domain reload happens finalizer would be a last point where NativeArray can be disposed for a ScriptableObject. It is not ideal scenario I would say, but can't think of other options ;)

    Code (CSharp):
    1. public void OnAfterDeserialize()
    2. {
    3.     if (!data.IsCreated)
    4.         data = new NativeArray<byte>(serializedData, Allocator.Persistent);
    5. }
    6.  
    7. public void OnDisable()
    8. {
    9.     if (data.IsCreated)
    10.         data.Dispose();
    11. }
    12.  
    13. void ~Test()
    14. {
    15.     if (data.IsCreated)
    16.         data.Dispose();
    17. }
     
    Thaina likes this.