Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

NativeArray, IComponentData and blittables

Discussion in 'Entity Component System' started by Afonso-Lage, Jun 1, 2018.

  1. Afonso-Lage

    Afonso-Lage

    Joined:
    Jul 8, 2012
    Posts:
    70
    Sup folks,

    I know NativeArray and IComponentData works only with blittable types and as far I understood this is to ensure linear memory layout, which is one of premisses to have the performance by default. But what I don't get is why a struct containing a NativeArray isn't blittable:

    Code (CSharp):
    1.     public struct ChunkMesh
    2.     {
    3.         public NativeArray<float3> vertices;
    4.         public NativeArray<int> indices;
    5.         public NativeArray<float3> normals;
    6.  
    7.         public void Dispose()
    8.         {
    9.             vertices.Dispose();
    10.             indices.Dispose();
    11.             normals.Dispose();
    12.         }
    13.  
    14.         public Mesh ToMesh(bool dispose = true)
    15.         {
    16.             var mesh = new Mesh
    17.             {
    18.                 vertices = vertices.Select(v => v.ToVector3()).ToArray(),
    19.                 normals = normals.Select(v => v.ToVector3()).ToArray(),
    20.             };
    21.  
    22.             mesh.SetIndices(indices.ToArray(), MeshTopology.Triangles, 0);
    23.  
    24.             if (dispose)
    25.                 Dispose();
    26.  
    27.             return mesh;
    28.         }
    29.     }
    NativeArray only contains pointers and length (as far I understood), it can be copied arround with no problem, no?

    Some cases I was able to use fixed buffer and solved this problem, but there are cases when I would like to return many data on a Job, like the above struct, where I generate vertices, indices and normals on a IJobForEach and just create the Mesh objects on JobSystem.

    Is there some workarround to use NativeArray or any non-fixed buffer inside a Job?
     
  2. Afonso-Lage

    Afonso-Lage

    Joined:
    Jul 8, 2012
    Posts:
    70
    I was able to do it by creating my own blittable array. I'll stick with it until I find out a better solution.

    Code (CSharp):
    1. using System;
    2. using Unity.Collections;
    3. using Unity.Collections.LowLevel.Unsafe;
    4.  
    5. namespace Voxel
    6. {
    7.     unsafe public struct BlitableArray<T> : IDisposable where T : struct
    8.     {
    9.         private void* m_Buffer;
    10.         private int m_Length;
    11.         private Allocator m_AllocatorLabel;
    12.  
    13.         unsafe public void Allocate(T[] array, Allocator allocator)
    14.         {
    15.             Allocate(array.Length, allocator);
    16.  
    17.             for (var i = 0; i < Length; i++)
    18.                 this[i] = array[i];
    19.         }
    20.  
    21.         unsafe public void Allocate(int size, Allocator allocator)
    22.         {
    23.             m_AllocatorLabel = allocator;
    24.             m_Length = size;
    25.             var elementSize = UnsafeUtility.SizeOf<T>();
    26.             m_Buffer = UnsafeUtility.Malloc(size * elementSize, UnsafeUtility.AlignOf<T>(), allocator);
    27.         }
    28.  
    29.         unsafe public void Dispose()
    30.         {
    31.             UnsafeUtility.Free(m_Buffer, m_AllocatorLabel);
    32.         }
    33.  
    34.         unsafe public T this[int index]
    35.         {
    36.             get
    37.             {
    38. #if ENABLE_UNITY_COLLECTIONS_CHECKS
    39.                 if (m_AllocatorLabel == Allocator.Invalid)
    40.                     throw new ArgumentException("AutoGrowArray was not initialized.");
    41.  
    42.                 if (index >= Length)
    43.                     throw new IndexOutOfRangeException();
    44. #endif
    45.  
    46.                 return UnsafeUtility.ReadArrayElement<T>(m_Buffer, index);
    47.             }
    48.  
    49.             set
    50.             {
    51. #if ENABLE_UNITY_COLLECTIONS_CHECKS
    52.                 if (m_AllocatorLabel == Allocator.Invalid)
    53.                     throw new ArgumentException("AutoGrowArray was not initialized.");
    54.  
    55.                 if (index >= Length)
    56.                     throw new IndexOutOfRangeException();
    57. #endif
    58.                 UnsafeUtility.WriteArrayElement(m_Buffer, index, value);
    59.             }
    60.         }
    61.  
    62.         public T[] ToArray()
    63.         {
    64.             var res = new T[Length];
    65.  
    66.             for (var i = 0; i < Length; i++)
    67.                 res[i] = this[i];
    68.  
    69.             return res;
    70.         }
    71.  
    72.         public static implicit operator T[] (BlitableArray<T> array)
    73.         {
    74.             return array.ToArray();
    75.         }
    76.  
    77.         public int Length { get { return m_Length; } }
    78.     }
    79. }
    80.  
     
  3. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,555
    I think the main problem is because of varying length. Maybe many assumptions that an offset pointer system made requires a fixed size data (which enables high performance). If we allow `NativeArray` then each `IComponentData` is of different size, then jumping around with a pointer is not as simple as multiplying the offset anymore.

    For example the chunk system, for one archetype they know in advance how many entity will fit in a chunk immediately after knowing an archetype. Now if an archetype contains NativeArray you cannot do this anymore.
     
  4. Afonso-Lage

    Afonso-Lage

    Joined:
    Jul 8, 2012
    Posts:
    70
    But a NativeArray is fixed size: A pointer, a length and a label. There are others fields but they are used for safety check;

    Or i'm missing something?
     
  5. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,555
    NativeArray become fixed size only after allocation. But in IComponentData struct signature the point is to know the data shape, so that the allocation by the ECS system is correct. If you create an entity and the program see "NativeArray<int>" in your IComponentData, how large should it allocate?

    The point of IComponentData is like a predefined layout so that the pointer can operate on easily. But NativeArray contains a pointer to operate on another memory area, which is to be determined later. NativeArray in all is just a pointer + length + label, that is correct. And if you only want to store those data you could make an IComponentData with int + int + int

    But NativeArray has an ability to allocate a space so that that pointer can do things on those space. That ability to allocate space, and the allocated space from that is not a data, therefore it is unrelated to IComponentData. (Think of NativeArray as a utility, not the actual data)

    For example if I have this :

    struct Data : IComponentData
    {
    int a;
    int b;
    int c;
    }


    struct Data2 : IComponentData
    {
    float x;
    float y;
    }


    I made 5 entities with the same archetype of Data + Data2. Then the data layout system could do this from the get go :

    <[abc][abc][abc][abc][abc] ________ [xy][xy][xy][xy][xy] ________ >

    < > = a chunk, Unity allocate a fixed size chunk and use it until full, then expands to a new chunk with the same shape because it knows the archetype
    ______ = empty space for more entities, if you would like to create more with the same archetype

    Then iterating will be very predictable and fast.
    - What happened if you add one other type of IComponentData to only 1 out of 5 entities? It will be moved out to other chunk as it is not considered the same shape anymore. Keeping the other 4 still predictable.

    What if I can chage the signature of Data struct to this :

    struct Data : IComponentData
    {
    NativeArray<int> datas;
    }

    If this is allowed then we will have
    [____][_][__][_______][___] ________ [xy][xy][xy][xy][xy] ___________

    How long each __ should it be? And how many entities can we still fit when the left over is not the same for each component?
    1. We won't know until we actually SetComponentData at runtime and each one can be alloated in different length.
    2. If this is allowed, it would also make a mess of the data with known length ([xy]) to be able to move around.

    And if you really want to have different length of ints packed together at runtime, then you could use the FixedArray which each length considered a different type. For example adding a FixedArray of 3 ints to 2 out of 5 entities should move those 2 to the same new chunk together.

    Of course you could use this tactic to do like the diagram above at runtime, to attach all 5 entities with different size of FixedArray (so now it essentially equals to allowing NativeArray, in concept) but those 5 will now be in its own chunk as it is now a completely different archetype. It can't stay together as it defeats the benefit of ECS's quick iteration + pointer math.
     
    Last edited: Jun 1, 2018
    iamarugin, wang37921 and Afonso-Lage like this.
  6. Afonso-Lage

    Afonso-Lage

    Joined:
    Jul 8, 2012
    Posts:
    70
    Thanks for clarifying it, but for me, a pointer is just an address which can be stored in a variable. So a NativeArray is just int+int+int (Ignoring safety check members). What I'll do with this data is job-wise.

    I can't see the difference between a IComponentData which holds a struct(int, int, int) and one that holds another struct(void*, int, int)

    NativeArray have, at compile time, the same time that it would have on runtime, so on your example, if I have a struct

    Code (CSharp):
    1. struct Data : IComponentData
    2. {
    3.     NativeArray<int> datas;
    4. }
    The entity chunk would be something like:

    <[ptr,sz,label][ptr,sz,label][ptr,sz,label][ptr,sz,label][ptr,sz,label]_____>

    I think the NativeArray can't be used inside IComponentData for some safety reason, since it is really easy to forget about it and don't dispose.

    What make me think like that is the fact that I have my own NativeArray-like struct (I called it BlittableArray) working inside a IComponentData:

    Code (CSharp):
    1. public struct ChunkData : IComponentData
    2.     {
    3.         private BlitableArray<byte> m_buffer;
    4.  
    5.         public void Allocate()
    6.         {
    7.             m_buffer.Allocate(BUFFER_SIZE, Allocator.Persistent);
    8.         }
    9.  
    10.         public void Dispose()
    11.         {
    12.             m_buffer.Dispose();
    13.         }
    14.  
    15.         public int this[int x, int y, int z]
    16.         {
    17.             get
    18.             {
    19.                 var index = (x << 8) + (y << 4) + z;
    20.                 Debug.Assert(index < BUFFER_SIZE, string.Format("Invalid buffer index at: {0}, {1}, {2}", x, y, z));
    21.                 return m_buffer[index];
    22.             }
    23.             set
    24.             {
    25.                 var index = (x << 8) + (y << 4) + z;
    26.                 Debug.Assert(index < BUFFER_SIZE, string.Format("Invalid buffer index at: {0}, {1}, {2}", x, y, z));
    27.                 m_buffer[index] = (byte)value;
    28.             }
    29.         }
    30.  
    31.         public bool IsEmpty()
    32.         {
    33.             //Since we just want to check if is empty, check in blocks of 8 bytes (long)
    34.             for (var i = 0; i < BUFFER_SIZE; i += 8)
    35.             {
    36.                 if (m_buffer[i] > 0)
    37.                     return false;
    38.             }
    39.  
    40.             return true;
    41.         }
    42.     }
    Forgive me if i'm beign lazy at some point.
     
    deus0 likes this.
  7. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,555
    Ok I actually just get your idea now. You want to store the `NativeArray` in IComponentData to keep those pointer states and length and not storing the actual allocated data. And then you could inject those pointer for different systems to use the allocated data together.

    Then this is definitely the result of the safety reason. What if you inject 2 BlitableArray to 2 system which use those pointer to read/write the same area of data? No safety system can save you. I do actually have problems in transferring NativeArray from system to system in my game and would like a blittable ones, but to stay safe I will just use NativeArray in the same system and pass the resulting blittable data via ECS. If I really want someone else's NativeArray then I can use system injection, which still warns you if you read while other is writing to it.
     
    Afonso-Lage likes this.
  8. Afonso-Lage

    Afonso-Lage

    Joined:
    Jul 8, 2012
    Posts:
    70
    Indeed, this risk is what bothers me on this BlittableArray solution, but the System Injection would not work inside a IJobForEach, since I need to do some heavy calculations, I would like to do it in separated jobs, so I end-up using my BlittableArray.

    I'll find another solution to my IComponentData, maybe using System Injection as you stated and use this unsafe solution only where is really needed.

    Thanks!
     
    5argon likes this.
  9. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,555
    The catch with directly grabbing other system's NativeArray is that you are staying in the safety system but out of automatic dependency system. To fix this just remember and expose that system's `JobHandle` that you know using that `NativeArray` as a public field in addition to merging with inputdeps and return them. Then after injecting the system you get not just the array but the handle too so that you could .Complete before reading. I just remembered I also have a job system which creates a bunch of (variable length) float3 forming a LineRenderer's segment in threads, and I sent them to other system on the main thread to actually create the LineRenderer exactly this way.
     
    Afonso-Lage likes this.
  10. Necromantic

    Necromantic

    Joined:
    Feb 11, 2013
    Posts:
    116
    I am having the same problem. I am looking for a way to have a list or array of things in IComponentData but every Collection or array type I try in Unity is restricted due to blittability.
     
    deus0 likes this.
  11. 5argon

    5argon

    Joined:
    Jun 10, 2013
    Posts:
    1,555
    Since that day I noticed the existence of [NativeDisableUnsafePtrRestriction] attribute in the source code. From https://docs.microsoft.com/en-us/dotnet/framework/interop/blittable-and-non-blittable-types we also know that `IntPtr` is blittable. Therefore putting int* to represent multiple values of allocated data with that attribute in an unsafe IComponentData might work? (I did not actually try this myself) But still you will have to make your own version of NativeArray as you cannot take out the internal pointer from it.
     
  12. M_R

    M_R

    Joined:
    Apr 15, 2015
    Posts:
    559
    maybe, but you will circumvent the safety system as the same memory could then be accessed from multiple IComponentData and therefore from different thread if you schedule a parallel job, leading to race conditions and potential crashes.

    if you think you need multiple stuff on a component, consider instead to have multiple entities each with one of those data and an IComponentData/ISharedComponentData with the entity id of its parent to group them together.

    i.e.:
    Code (CSharp):
    1. public struct Parent : ISharedComponentData { public Entity value; }
    2.  
    3. // if you need ordering:
    4. public struct SiblingIndex : IComponentData { public int value; }
    5.  
    6. // your stuff
    7. public struct Foo : IComponentData { ...scalar values only... }
    8. public struct Bar : IcompoinentData { ...scalar values only... }
    9.  
    10. // archetypes
    11. var parent = EntityManager.CreateEntity(typeof(Foo), ...);
    12. var child = EntityManager.CreateEntity(typeof(Parent), typeof(Bar), ...);
    13. EntityManager.SetComponentData(child, new Parent{value = parent});
    14.  
    15. // "get children" call:
    16. group.SetFilter(new Parent {value = ...});
    17. foreach (var child in group) {do stuff}
     
  13. Afonso-Lage

    Afonso-Lage

    Joined:
    Jul 8, 2012
    Posts:
    70
    It works, but it's horrible to work with. I've made my own BlittableArray to add it on ComponentData, but every time I made a mistake, Unity just crashes, since I'm working with unsafe code. So I'll try my best to avoid using it.
     
  14. BrianWill

    BrianWill

    Joined:
    Oct 10, 2014
    Posts:
    38
    I think @M_R hits the real reason: job safety checks wouldn't work (or would be super expensive) if we could have native containers in our components.

    Also, I believe native containers are not technically blittable because it's a two-way street. A blittable type is one which:

    1. can be moved from the managed world to native world with no issue
    2. AND can be moved from the native world to managed world with no issue

    If a native container were moved to the managed world, properly then the garbage collector should be responsible for disposing of it, but that would require 'conversion', not just taking the native container struct as is.
     
  15. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    760
    I suspect many people who find this page might be wondering if it's possible to have a FIXED sized array in IComponentData. The topic is slightly different than Alfonso's dilemma, but it's similar and this post is the first one that comes up when you search for putting arrays into IComonentData. I think putting some fixed array information here might help some people.

    Here's a common example: you want to store 200 or less float3 items in a fixed sized waypoints array. In memory it would look like [x0,y0,z0][x1,y1,z1][x2,y2,z2]....[x199,y199,z199]. Because the size (200) is known up-front, iterating chunks would still be simple and fast and indexes for skipping over chunks won't have any problems. I get why NativeArray doesn't work - because it's not fixed at declaration time. What I don't get is why I can't find an obvious standard accepted C# or Unity mechanism to do this when it seems so simple! It's understandable that C# is trying to be safe, but having a fixed size array in this context seems safe to me. Am I missing something obvious?


    Option 1) Use unsafe-mode in order to make a fixed c-style static array.
    Code (CSharp):
    1. internal unsafe struct pointsX { private fixed float waypointsX[200]; }
    2. internal unsafe struct pointsY { private fixed float waypointsY[200]; }
    3. internal unsafe struct pointsZ { private fixed float waypointsZ[200]; }
    It won't let you use float3, but it'll let you use floats (hence 3 arrays instead of one). The main problem is that you'll need to compile with /unsafe - making the entire game not work in webgl. That's no bueno.


    Option 2) Declare every single variable explicitly:
    Code (CSharp):
    1. float3 waypointX01;
    2. float3 waypointY01;
    3. float3 waypointZ01;
    4. .......
    5. float3 waypointX200;
    6. float3 waypointY200;
    7. float3 waypointZ200;
    This is beyond stupid.


    Option 3) Use DynamicBuffer and make a PointBufferElement.
    Code (CSharp):
    1. public struct PointBufferElement : IBufferElementData  { public float3 value; }
    One problem with this is that it doesn't reside with the rest of your component data so it's not as clean. The main problem is that doing a Entities.ForEach() won't allow you to get the entity at the same time as a dynamic buffer. This works:
    Code (CSharp):
    1. Entities.ForEach((DynamicBuffer<PointBufferElement> buf, ref Translation pos, ref MyComponent data);
    But this does not
    Code (CSharp):
    1. Entities.ForEach(Entity e, (DynamicBuffer<PointBufferElement> buf, ref Translation pos, ref MyComponent data);

    Option 5) This option stems from Unity.Collections containing FixedListFloat32, FixedListFloat64, etc. I found this by accident and as far as I can tell, there's next to zero documentation on it and unlike with UE4 or Godot, you can't view the source code. Because of this fact, I have no idea as to how fast it is, except for one anecdotal story relayed by someone in the forums saying they thought it was fast. I believe ForEach() uses it internally because if you step into a ForEach() while debugging, you'll see some FixedLists. Overall, this method actually turns out to not be too bad.
    Code (CSharp):
    1. public struct MyComponent : IComponentData { public FixedList4096<float3> waypoints; }
    2. ....
    3. myComponent.waypoints.Add(path.vectorPath[i]);
    This is similar to what I was looking for. I question the sanity of the design pattern when the quantity is hard-coded into the type name like "FixedList128" - seems janky. However, it works and I can't find anything better. IMPORTANT NOTE - Intuitively, one would think that FixedList32<float3> means that you will have a fixed list with 32 items. NOPE! It's 32 bytes - not items. So a FixedList32<float3> can only hold 2 items. The largest one I saw was FixedList4096, which allows storage of up to 341 float3 items. If the limit is reached, an exception will be thrown. From what I understand, you don't want to have too much data stored in each component because it'll cause some sort of issue with chunk sizes and cache missing that hurts performance. I suspect DynamicBuffer might have the same issue under-the-hood though. I don't have enough data to say which is faster.
     
  16. JooleanLogic

    JooleanLogic

    Joined:
    Mar 1, 2018
    Posts:
    447
    Source for them is in FixedList.gen.cs.

    Buffer is stored in chunk as stream just like any other component (with some caveats) so they're much the same storage wise although fixed arrays on components is definitely easier to work with than the DynamicBuffer interface.

    Performance wise, you can cast buffers as NativeArray at which point I think the code is probably the same. You'd have to test but from memory both do bounds checking.
    You can find source for buffer and NativeArray somewhere as well.
     
    lclemens likes this.
  17. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    760
    Cool - I was wrong about the source code for FixedList. I wasn't able to go to definition for some reason, but FixedList.gen.cs isn't hidden behind the engine. If I do any performance testing between DynamicBuffer<float3> and FixedList4096<float3> I'll report back what I find here.
     
    JJaime and deus0 like this.
  18. deus0

    deus0

    Joined:
    May 12, 2015
    Posts:
    256
    Hey its been 2 years, is there any alternative by unity or whatever else to BlitableArray? I'm kinda depending on them too much. Fixed arrays arn't too useful because i'de like to add unique stats to characters based on game events.
    Are there any performance limitations using BlitableArrays? I mostly update lengths in the main thread with systembase and query.withoutburst.run(); etc.
     
  19. burningmime

    burningmime

    Joined:
    Jan 25, 2014
    Posts:
    845
    If you're sure of your thread safety, store a pointer and length. If it's read-only in the job, blob asset references will also work without using any unsafe code.
     
    deus0 likes this.
  20. deus0

    deus0

    Joined:
    May 12, 2015
    Posts:
    256
    Hi Burning, Just to make sure, using multiple JobSystems with these pointers, as long as you create the job systems they generally run per system right? (The JobSystems will run each job in parralel, but chain them, so every system will run in sequence as the main thread handles Job Scheduling)
    So far I haven't had any problems using these Blitable arrays everywhere in my components. I guess I wonder why unity doesn't have their own solution for this? Native Arrays are fine but we cannot use them in jobs with burst so they aren't performant.
     
  21. JJaime

    JJaime

    Joined:
    Jan 21, 2020
    Posts:
    1
    Hello,
    the report of performance comparison between DynamicBuffer and FixedList is interesting, did you make your trials?
     
  22. lclemens

    lclemens

    Joined:
    Feb 15, 2020
    Posts:
    760
    No i haven't gotten around to it. I used fixedlist4096 for my waypoints and it worked so cleanly without any performance bottlenecks that I just left it like that and moved on.