Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice
  3. Join us on November 16th, 2023, between 1 pm and 9 pm CET for Ask the Experts Online on Discord and on Unity Discussions.
    Dismiss Notice
  4. Dismiss Notice

How to manage multiple services updating same native array

Discussion in 'Entity Component System' started by mike1997, Apr 13, 2021.

  1. mike1997

    mike1997

    Joined:
    Jul 1, 2016
    Posts:
    25
    Hello,

    In my setup I have a native array that's holding item positions, I have multiple SystemBases running that all use this native array, which is a static variable so easy to access.

    Now to update the native array I need to make sure that all the jobs that read from it are done, then start updating, now if its all in one file, in one systembase, using dependencies is pretty simple, but how do I manage multiple jobs that need to be done for my write service to start. I tried
    [UpdateBefore(typeof(..)] but that doesn't guarantee that the jobs have stopped running, so no go.

    Any help would be greatly appreciated.

    *** As suggested I modified my question changed services to jobs, to make it easier for future readers

    Mike
     
    Last edited: Apr 14, 2021
  2. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,631
    So my rule of thumb is, never get into this situation because it will lead to years of debugging hell, as it's nearly always a sign of bad design.

    However, if you insist living in this hell the answer is you need to manually manage dependencies, something like

    Code (CSharp):
    1. public JobHandle GetArray(JobHandle inputDeps, out NativeArray<T> array)
    2. {
    3.     array = this.array;
    4.     this.dependency = JobHandle.CombineDependencies(this.dependency, inputDeps)
    5.     return this.dependency;
    6. }
    Note manually handling dependencies like this has a good chance of reducing your application throughput by having all your jobs out of order.
     
  3. mike1997

    mike1997

    Joined:
    Jul 1, 2016
    Posts:
    25
    Thanks for reply

    In my situation I have a native array of grid cells, cell struct is simple, just has position, walking speed and object id if there is something on that cell.

    Now I would have multiple services that would need to read this array, for example path finding, placing objects, so forth.

    Would you use a native array to hold this? Whats a better design solution?

    I really appreciate your advice, thx.
     
  4. vectorized-runner

    vectorized-runner

    Joined:
    Jan 22, 2018
    Posts:
    383
    Why UpdateBefore doesn't guarantee? I'd make 2 ComponentSystemGroups one for writing and reading and update reading after writing
     
  5. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,631
    I use a navmesh and just store it on a singleton so safety is handled automatically.

    Now there's a bit more complexity going on here as I use NativeArrays in the NavMeshWorlds but this is obviously not doable on a component so I have this NativeArrayProxy that lets me store native arrays on components and then regenerate the safety

    So to use it, it's just

    Code (CSharp):
    1. var navMeshWorlds = GetSingleton<NavMeshWorldsProxy>().ToNavMeshWorld();
    Full code below

    Code (CSharp):
    1.     public readonly unsafe struct NavMeshWorldsProxy : IComponentData
    2.     {
    3.         private readonly NativeArrayProxy<NavMeshWorld> worlds;
    4.  
    5.         [NativeDisableUnsafePtrRestriction]
    6.         private readonly AtomicSafetyManager* safetyManager;
    7.  
    8.         internal NavMeshWorldsProxy(NavMeshWorlds world, SharedSafety sharedSafety)
    9.         {
    10.             this.worlds = new NativeArrayProxy<NavMeshWorld>(world.Worlds);
    11.             this.safetyManager = sharedSafety.SafetyManager;
    12.         }
    13.  
    14.         public NavMeshWorlds ToNavMeshWorld()
    15.         {
    16.             return new NavMeshWorlds { Worlds = this.worlds.ToArray(this.safetyManager) };
    17.         }
    18.     }
    Code (CSharp):
    1.  
    2.     public readonly unsafe struct NativeArrayProxy<T>
    3.         where T : struct
    4.     {
    5.         private readonly void* ptr;
    6.         private readonly int length;
    7.  
    8.         public NativeArrayProxy(NativeArray<T> nativeArray)
    9.         {
    10.             this.ptr = nativeArray.GetUnsafeReadOnlyPtr();
    11.             this.length = nativeArray.Length;
    12.         }
    13.  
    14.         public NativeArray<T> ToArray(AtomicSafetyManager* safetyManager)
    15.         {
    16.             var nativeArray = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray<T>(this.ptr, this.length, Allocator.Invalid);
    17.             safetyManager->MarkNativeArrayAsReadOnly(ref nativeArray);
    18.             return nativeArray;
    19.         }
    20.     }
    AtomicSafetyManager (and SharedSafety) is based off the one inside the unity physics package. You just bump (sync) the version of SharedSafety and write to the singleton once a frame in the system responsible for updating your grid.

    My automatically handled safe world grid system!
     
    Last edited: Apr 13, 2021
  6. mike1997

    mike1997

    Joined:
    Jul 1, 2016
    Posts:
    25
    Really Interesting, I will need some time to study this, thanks



    It seems this doesn't work with dependencies. Maybe it guarantees when services start, but I guess doesn't wait till they end.
     
  7. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,294
    Please stop calling jobs - "services". Its really confusing.

    This is true. UpdateAfter / UpdateBefore does not CombineDependencies automatically, but nobody stops you from exposing JobHandle.

    E.g. that can be done like this:
    https://forum.unity.com/threads/how...ating-same-native-array.1092358/#post-7033324

    Or like this:
    https://forum.unity.com/threads/doe...pendency-job-completion.1087040/#post-7004555

    And performing JobHandle.CombineDependencies on Dependency, *handleFromDifferentSystem* manually.
     
  8. Micz84

    Micz84

    Joined:
    Jul 21, 2012
    Posts:
    436
    Wow!That is clever. I will have to try use that trick in my flow field implementation.
     
  9. mike1997

    mike1997

    Joined:
    Jul 1, 2016
    Posts:
    25
    I have been thinking about his example for some time, trying to understand it, and still have a few questions

    1. Getting the singleton allows the system to manage dependencies, now if i have many jobs that only read, is it possible to use get singleton as readonly?
    2. Is it possible to have nativehashmap proxy, it seems that all the unsafe utilities are only for nativearrays?
    3. I also understand the static variable hell, especially when working with multiple worlds, and to my understanding the answer to #2 is no, so is there a better place to store my nativehashmaps? Blobs is an option, but to my understanding its only for data that doesnt change often, so anywhere else?


    Again thank you everyone for all the help
     
  10. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,631
    GetSingleton is read only

    Code (CSharp):
    1. public T GetSingleton<T>()
    2.     where T : struct, IComponentData
    3. {
    4.     var type = ComponentType.ReadOnly<T>();
    5.     var query = GetSingletonEntityQueryInternal(type);
    6.     return query.GetSingleton<T>();
    7. }

    Yes I have one of these as well (though I don't currently use it for anything) but you need internal access to collections. You just pull out the map it's using.

    Code (CSharp):
    1.     public readonly unsafe struct NativeHashMapProxy<TKey, TValue>
    2.         where TKey : struct, IEquatable<TKey>
    3.         where TValue : struct
    4.     {
    5.         private readonly UnsafeHashMap<TKey, TValue> hashmap;
    6.  
    7.         public NativeHashMapProxy(NativeHashMap<TKey, TValue> nativeHashMap)
    8.         {
    9.             this.hashmap = nativeHashMap.m_HashMapData;
    10.         }
    11.  
    12.         public NativeHashMap<TKey, TValue> ToNativeHashMap(AtomicSafetyManager* safetyManager)
    13.         {
    14.             var nativeHashMap = new NativeHashMap<TKey, TValue> { m_HashMapData = this.hashmap };
    15.             safetyManager->MarkNativeHashMapAsReadOnly(ref nativeHashMap);
    16.             return nativeHashMap;
    17.         }
    18.     }
    Note, I forgot to mention how to manage memory. Make sure the system responsible for writing the proxy is also the system that creates the original container and keep a copy so you can dispose it on cleanup (not the proxy).
     
    Last edited: Apr 15, 2021
  11. mike1997

    mike1997

    Joined:
    Jul 1, 2016
    Posts:
    25
    I beg your patience, but I have more questions:

    Trying out this code, it seems that AtomicSafetyManager implementation is not publicly available, I searched through unity docs, but couldn't find it, the only google result is to your other post where you share some code :)
    Am I correct in this assumption?


    Also if getSingleton is read only, how do I tell my BaseSystem that in these jobs I want readwrite access? For cases where I do want to modify it? I'm sure this is super simple, just trying to get my head to fully understand how entities work.
     
  12. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,631
    My implementation is based off the Unity.Physics which you can get from your package cache.
    com.unity.physics@0.6.0-preview.3\Unity.Physics\Base\Containers\AtomicSafetyManager.cs
    The only difference is I added HashMap support.
    It's a private struct so you'll need to make your own copy in your project.

    SetSingleton or any SetComponent etc will make it write.
     
  13. PublicEnumE

    PublicEnumE

    Joined:
    Feb 3, 2019
    Posts:
    729
    Regarding the OP’s question of how to handle multiple producers writing to the same NativeArray:

    I’m surprised no one has suggested just chunking the array. If you know the number of producer jobs that will need to add to the array, and an upper limit to how many elements each one might need to add, you can section off chunks of the array so that no two jobs can ever write to the same index.

    In that case, all writing jobs could do their writing in parallel, as well.

    Since this wasn’t suggested, perhaps I have misunderstood the OP’s needs.
     
  14. mike1997

    mike1997

    Joined:
    Jul 1, 2016
    Posts:
    25
    Thank you for the reply, I spend last couple of days implementing this pattern.

    At this stage I need both, use this to split up my work into different array segments, and also I need to manage different jobs.

    I have been thinking about this for a while, and while implementing my own singleton array managing singleton is out of my range right now, I thought about a middle ground.

    I already tried it, it works, just want to see if this is an efficient way of ding it, especially the part where I call Dependency.Complete().

    I create a dummy singleton. then on the reading array jobs, after the Dependency.Complete() line, I just make a dummy call to the singleton and read it, this makes unity assume that this job relies on and reads from the singleton.
    Then on the job that writes the array, at the beginning I modify the dummy singleton, this tells unity that I'm writing it and makes sure that all the jobs that read from it are fully done.

    Now this works, but I always assumed that calling the Dependency.Complete() was not a good practice. Am I correct in this assumption, should I really be trying to avoid this part:
    Dependency.Complete();
    var temp= GetSingleton<SingletonComp>();

    Any advice would be greatly appreciated.
     
    Egad_McDad and PublicEnumE like this.
  15. mike1997

    mike1997

    Joined:
    Jul 1, 2016
    Posts:
    25

    Today I felt brave enough to give this another try, and ended up with more questions, I really appreciate your help.

    This is what I got so far,its not exact same as the sample provided, I guess the version of physics library that Im referencing is a bit different, but hope it still works:


    Code (CSharp):
    1. public struct GridDataSingleton : IComponentData
    2.     {
    3.  
    4.         private readonly NativeArrayProxy<Cell> cells;
    5.        
    6.         [NativeDisableUnsafePtrRestriction]
    7.         private readonly unsafe AtomicSafetyManager* safetyManager;
    8.        
    9.         internal unsafe GridDataSingleton(NativeArray<Cell> cells)
    10.         {
    11.             this.cells = new NativeArrayProxy<Cell>(cells);
    12.             safetyManager = Utility.CAlloc<AtomicSafetyManager>(Allocator.Persistent);
    13.             *safetyManager = AtomicSafetyManager.Create();
    14.            
    15.      
    16.         }
    17.        
    18.         public void Dispose()
    19.         {
    20.             unsafe
    21.             {
    22.                 safetyManager->Dispose();
    23.             }
    24.         }
    25.        
    26.     }
    27.    

    And my proxy:


    Code (CSharp):
    1. public readonly unsafe struct NativeArrayProxy<T>
    2.         where T : struct
    3.     {
    4.         readonly void* Ptr;
    5.         readonly int Length;
    6.         public bool IsCreated => Ptr != null;
    7.         public NativeArrayProxy(NativeArray<T> nativeArray)
    8.         {
    9.             this.Ptr = nativeArray.GetUnsafeReadOnlyPtr();
    10.             this.Length = nativeArray.Length;
    11.         }
    12.        
    13.         public NativeArrayProxy(NativeSlice<T> nativeSlice)
    14.         {
    15.             Ptr = nativeSlice.GetUnsafeReadOnlyPtr();
    16.             Length = nativeSlice.Length;
    17.         }
    18.         public NativeArray<T> ToArray(AtomicSafetyManager* safetyManager)
    19.         {
    20.             var nativeArray = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray<T>
    21.                 (this.Ptr, this.Length, Allocator.Invalid);
    22.             safetyManager->MarkNativeArrayAsReadOnly(ref nativeArray);
    23.             return nativeArray;
    24.         }
    25.     }


    Now here are some things I still am not sure of:

    - ToArray functions makes the array readonly, but I don't see any other functions for read-write, how would I modify the array
    - safetyManager->BumpTemporaryHandleVersions(); -- I'm assuming ill need this function after modifying the array right?

    - Also
    safetyManager = Utility.CAlloc<AtomicSafetyManager>(Allocator.Persistent);
    *safetyManager = AtomicSafetyManager.Create();
    This is the only reference I could find where a pointer to AtomicSafetyManager was created, would this still work?

    I really appreciate all the help, this is a bit over my help, but I also can see my game code being greatly reduced and simplified if I can just make this work.

    Thx again.
     
  16. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,631
    It's read only, that's kind of the point of this setup. You should have the original array stored in your system that writes to the grid (not created on the component) and then use the singleton to provide a read only copy to any external system without the need to handle safety. Any writing should only be done by the original system that created the array (single responsibility etc etc.)

    That said, I think it's fine to have multiple systems within the same assembly write to the array as the assembly has complete knowledge over the requirements - it's just when you start doing arbitrary writing from external systems that things become a complete mess. You'll just have to handle those dependencies separate.

    Yes. Mine just looks like this.

    Code (CSharp):
    1. this.SetComponent(this.navMeshWorldsEntity, new NavMeshWorldsProxy(this.navMeshWorlds, this.sharedSafety));
    2. this.sharedSafety.Sync();
    3.  
    Pretty much. If you look in ExportPhysicsWorld you'll see this SharedDataStruct which wraps all the unsafe code for you

    Code (CSharp):
    1. internal unsafe struct SharedData : IDisposable
    2.         {
    3.             [NativeDisableUnsafePtrRestriction]
    4.             public AtomicSafetyManager* SafetyManager;
    5.  
    6.             public static SharedData Create()
    7.             {
    8.                 var sharedData = new SharedData();
    9.                 sharedData.SafetyManager = (AtomicSafetyManager*)UnsafeUtility.Malloc(UnsafeUtility.SizeOf<AtomicSafetyManager>(), 16, Allocator.Persistent);
    10.                 *sharedData.SafetyManager = AtomicSafetyManager.Create();
    11.  
    12.                 return sharedData;
    13.             }
    14.  
    15.             public void Dispose()
    16.             {
    17.                 SafetyManager->Dispose();
    18.             }
    19.  
    20.             public void Sync()
    21.             {
    22.                 SafetyManager->BumpTemporaryHandleVersions();
    23.             }
    24.         }
     
  17. mike1997

    mike1997

    Joined:
    Jul 1, 2016
    Posts:
    25
    That helps a lot, im getting there, thanks for the reply