Search Unity

Allow setting mesh arrays with NativeArrays.

Discussion in 'Data Oriented Technology Stack' started by wusticality, Jun 19, 2018.

  1. wusticality

    wusticality

    Joined:
    Dec 15, 2016
    Posts:
    47
    Hey folks,

    I have a feature request for the Unity dev team. It would be very nice if you could add some overloads to the Mesh class so that we could set triangles, vertices and uvs using references to NativeArrays. I'm currently building my geometry in a job, but I have to convert the NativeArray data to an array to give to the Mesh class (which no doubt marshalls the data back to native) which is horribly inefficient.

    Any plans to add this functionality? Also please give me a heads up if there is an equivalent approach I'm not aware of.

    Cheers, -gormulent
     
    dartriminis likes this.
  2. Lurking-Ninja

    Lurking-Ninja

    Joined:
    Jan 20, 2015
    Posts:
    4,551
    They will do that. You can find the answer in one of threads about the job system.
     
  3. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,722
    You can actually modify arrays (int[] and Vector3[] etc) within jobs using pointers. This is not great but at the moment it's the best work around to avoid the slow copy (a magnitude of performance increase.)

    keijiro has an example of github: https://github.com/keijiro/Firefly

    The general idea is this

    Code (CSharp):
    1.         [BurstCompile]
    2.         private struct GenerateMeshJob : IJob
    3.         {
    4.             // ...
    5.  
    6.             // basically a wrapper for a int pointer, from https://github.com/keijiro/Firefly
    7.             public NativeCounter.Concurrent Counter;
    8.  
    9.             // Pointers to the start of an array
    10.             [NativeDisableUnsafePtrRestriction] public void* Vertices;
    11.  
    12.             public void Execute()
    13.             {
    14.                 // ...
    15.                 UnsafeUtility.WriteArrayElement(Vertices, vertCount + n, vertices);
    16.                 // ...
    17.             }
    18.         }
    19.  
    20.         // Job creation
    21.         var generateMeshJob = new GenerateMeshJob
    22.         {
    23.             Vertices = UnsafeUtility.AddressOf(ref vertices[0]),
    24.         }
    25.         .Schedule(getVisibleFacesJob);
    From this I figured out you can actually do it for Lists as well (using the internal array lists hold) which I needed because I wanted to use the SetUV(List<Vector3>).

    This is a little more complicated as it's either super slow or creates garbage if you're not careful. I was intending to write a blog post / tutorial on doing all this later this week.

    I've managed to get a nice voxel engine running generating meshes for 400k voxels in 2.8ms on an older 3570k. It takes 4x longer to upload the changed mesh to the GPU than it does to generate it.
     
    Last edited: Jun 19, 2018
  4. wusticality

    wusticality

    Joined:
    Dec 15, 2016
    Posts:
    47
    That seems pretty unsafe unless you're damn certain that you're adding the right job dependencies ..

    At any rate, I'm hoping they add overloads for this, it's silly to prepare this data in C# arrays, the marshalling cost is just too high.
     
  5. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,722
    It is called unsafe for a reason!

    That said, as much as I hate the idea of having to use pointers in c#, you got to do what you got to do.
    As far as I'm aware, it's the best (and only) way to do this at the moment.
    They will eventually add support for meshes (for example Texture2D has support now) but until we have to suck it up or wait.

    I was considering writing a wrapper to hide this process in the background, a bit like the concurrent counter.
     
  6. wusticality

    wusticality

    Joined:
    Dec 15, 2016
    Posts:
    47
    I'm not talking about the use of pointers, I'm a veteran C++ engine programmer haha - I'm talking about threading issues.
     
  7. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,722
    Oh sure, but it's not really much worse than writing to a NativeArray with [NativeDisableParallelForRestriction] which is pretty common behavior.
     
  8. LennartJohansen

    LennartJohansen

    Joined:
    Dec 1, 2014
    Posts:
    2,311
    Based on this example I modified the CopyToFast extention to do a single Memory copy call.
    This way you can create the NativeArrays in jobs as normal and then just do a fast copy to the managed array before adding. You still have the overhead of the extra memory copy but is faster than the current CopyTo implementation.

    Code (csharp):
    1.  
    2.     public static class NativeArrayExtensions
    3.     {
    4.         public static unsafe void CopyToFast<T>(
    5.             this NativeArray<T> nativeArray,
    6.             T[] array)
    7.             where T : struct
    8.         {
    9.             if (array == null)
    10.             {
    11.                 throw new NullReferenceException(nameof(array) + " is null");
    12.             }
    13.  
    14.             int nativeArrayLength = nativeArray.Length;
    15.             if (array.Length < nativeArrayLength)
    16.             {
    17.                 throw new IndexOutOfRangeException(
    18.                     nameof(array) + " is shorter than " + nameof(nativeArray));
    19.             }
    20.  
    21.             int byteLength = nativeArray.Length * Marshal.SizeOf(default(T));
    22.             void* managedBuffer = UnsafeUtility.AddressOf(ref array[0]);
    23.             void* nativeBuffer = nativeArray.GetUnsafePtr();
    24.             Buffer.MemoryCopy(nativeBuffer, managedBuffer, byteLength, byteLength);
    25.         }
    26. }
    EDIT:

    I did some speed testing and replaced the buffer.MemoryCopy with this on windows platform

    Code (csharp):
    1.  
    2. #if PLATFORM_STANDALONE_WIN
    3.         [DllImport("msvcrt.dll", EntryPoint = "memcpy")]
    4.         public static extern void CopyMemory(IntPtr pDest, IntPtr pSrc, int length);
    5. #endif
    6.  
     
    Last edited: Jun 23, 2018
  9. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,722
    I did something similar except I extended it and created my own UnsafeNativeArray to completely avoid having to do any copy.

    Here are some performance comparisons

    Default NativeArray.CopyTo (2.40ms)



    Array.CopyTo - very similar to Marshal performance (0.15ms)
    Similar approach to @LennartJohansen, much much faster compared to default implementation



    My custom UnsafeNativeArray - directly editing the array, no copying (0.07ms)



    So relative performance between Array.Copy and directly editing array isn't huge, however it does halve your memory requirement as you don't require 2 copies of the array - not a big deal if you only need a single array but if you need a lot it's signficant.

    If anyone wants to try my UnsafeNativeArray, I've uploaded it here: https://github.com/tertle/UnsafeNativeArray

    It works basically the same as NativeArray (90% of the code is identical) except you pass it an array in the constructor. It includes the same checks. It's not safe because there are no checks on using the array you pass in. You must complete the job before using this array otherwise you'll run into trouble.

    Code (CSharp):
    1. var array = new int[30];
    2. var unsafeNativeArray = new UnsafeNativeArray<int>(array);
    3.  
    4. var job = new Job
    5. {
    6.     UnsafeNativeArray = unsafeNativeArray; // use UnsafeNativeArray just like a NativeArray
    7. }.Schedule();
    8.  
    9. job.Complete(); // make sure job is complete before using array
    10.  
    11. // Use array how you like, no need to copy result as UnsafeNativeArray just directly modifies the array
    -edit-
    one thing to note is you can't use a 0 length array with UnsafeNativeArray, will throw an exception.
     
    Last edited: Jun 24, 2018
    Babiole, 5argon and SugoiDev like this.
  10. LennartJohansen

    LennartJohansen

    Joined:
    Dec 1, 2014
    Posts:
    2,311
    Loo
    Looks good.

    I did not try it yet but I guess you could also get a pointer to the managed arrays first element and create the array from this.

    Code (CSharp):
    1.  
    2.  void* managedBuffer = UnsafeUtility.AddressOf(ref array[0]);
    3. NativeArray<T> newArray = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray<T>(managedBuffer,array.length,Allocator.None)
    4.  
    My guess is allocator.None since the managed array keeps the memory reference and will free it.
     
  11. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,722
    Oh interesting. Did not know that existed. Going to go exploring.

    -edit-

    seems to mostly do what I did so I don't need a separate class for it then

    that's awesome, cheers for that

    -edit2-

    can't seem to get it to work. job complains it's not assigned or constructed, even though I can see it fully allocated. will keep exploring
     
    Last edited: Jun 24, 2018
  12. LennartJohansen

    LennartJohansen

    Joined:
    Dec 1, 2014
    Posts:
    2,311
    did it work with just the address of the array? or do you need to use

    GCHandle.Alloc(managedArray, GCHandleType.Pinned);
    and get the pointer from that?
     
  13. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,722
    Ok. Got it (kind of) working. You need to setup the safety as well.

    Code (CSharp):
    1.             AtomicSafetyHandle safety;
    2.             DisposeSentinel disposeSentinel;
    3.             DisposeSentinel.Create(out safety, out disposeSentinel, 1);
    4.             NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref nativeArray, safety);
    Just using UnsafeUtility.AddressOf(ref array[0]); worked fine

    The issue is, there does not seem to be a way to also pass the disposeSentinel in - I'm not sure why there is only a method for the safety. So I don't think it's going to dispose correctly.
     
  14. LennartJohansen

    LennartJohansen

    Joined:
    Dec 1, 2014
    Posts:
    2,311
    I will give it a test tomorrow also. This could be a good way to serialize data. Unity can manage the saving to a scriptable object and the job system can work on data with the nativearray you create from the array.
     
  15. prvncher

    prvncher

    Joined:
    Dec 10, 2016
    Posts:
    1
    Hey Tertle - Any chance you've written that blog post? Thanks for this post
     
  16. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,722
    Nah never got around to it. Happy to answer any questions though.

    I don't use this approach anymore as DynamicBuffers now exist that removes most of the need for this.

    So now I use buffers and wrote an extension method for List<T>.AddRange(DynamicBuffer<T>)

    Code (CSharp):
    1. namespace BovineLabs.Common.Native
    2. {
    3.     using System;
    4.     using System.Collections.Generic;
    5.     using BovineLabs.Common.Utility;
    6.     using Unity.Collections;
    7.     using Unity.Collections.LowLevel.Unsafe;
    8.     using Unity.Entities;
    9.  
    10.     /// <summary>
    11.     /// Extensions for Native Containers.
    12.     /// </summary>
    13.     public static class Extensions
    14.     {
    15.         /// <summary>
    16.         /// Adds a native version of <see cref="List{T}.AddRange(IEnumerable{T})"/>.
    17.         /// </summary>
    18.         /// <typeparam name="T">The type.</typeparam>
    19.         /// <param name="list">The <see cref="List{T}"/> to add to.</param>
    20.         /// <param name="array">The native array to add to the list.</param>
    21.         public static unsafe void AddRange<T>(this List<T> list, NativeArray<T> array)
    22.             where T : struct
    23.         {
    24.             AddRange(list, array, array.Length);
    25.         }
    26.  
    27.         /// <summary>
    28.         /// Adds a native version of <see cref="List{T}.AddRange(IEnumerable{T})"/>.
    29.         /// </summary>
    30.         /// <typeparam name="T">The type.</typeparam>
    31.         /// <param name="list">The <see cref="List{T}"/> to add to.</param>
    32.         /// <param name="array">The array to add to the list.</param>
    33.         /// <param name="length">The length of the array to add to the list.</param>
    34.         public static unsafe void AddRange<T>(this List<T> list, NativeArray<T> array, int length)
    35.             where T : struct
    36.         {
    37.             list.AddRange(array.GetUnsafeReadOnlyPtr(), length);
    38.         }
    39.  
    40.         /// <summary>
    41.         /// Adds a native version of <see cref="List{T}.AddRange(IEnumerable{T})"/>.
    42.         /// </summary>
    43.         /// <typeparam name="T">The type.</typeparam>
    44.         /// <param name="list">The <see cref="List{T}"/> to add to.</param>
    45.         /// <param name="nativeList">The native list to add to the list.</param>
    46.         public static unsafe void AddRange<T>(this List<T> list, NativeList<T> nativeList)
    47.             where T : struct
    48.         {
    49.             list.AddRange(nativeList.GetUnsafePtr(), nativeList.Length);
    50.         }
    51.  
    52.         /// <summary>
    53.         /// Adds a native version of <see cref="List{T}.AddRange(IEnumerable{T})"/>.
    54.         /// </summary>
    55.         /// <typeparam name="T">The type.</typeparam>
    56.         /// <param name="list">The <see cref="List{T}"/> to add to.</param>
    57.         /// <param name="nativeSlice">The array to add to the list.</param>
    58.         public static unsafe void AddRange<T>(this List<T> list, NativeSlice<T> nativeSlice)
    59.             where T : struct
    60.         {
    61.             list.AddRange(nativeSlice.GetUnsafeReadOnlyPtr(), nativeSlice.Length);
    62.         }
    63.  
    64.         /// <summary>
    65.         /// Adds a native version of <see cref="List{T}.AddRange(IEnumerable{T})"/>.
    66.         /// </summary>
    67.         /// <typeparam name="T">The type.</typeparam>
    68.         /// <param name="list">The <see cref="List{T}"/> to add to.</param>
    69.         /// <param name="dynamicBuffer">The dynamic buffer to add to the list.</param>
    70.         public static unsafe void AddRange<T>(this List<T> list, DynamicBuffer<T> dynamicBuffer)
    71.             where T : struct
    72.         {
    73.             list.AddRange(dynamicBuffer.GetUnsafePtr(), dynamicBuffer.Length);
    74.         }
    75.  
    76.         /// <summary>
    77.         /// Adds a range of values to a list using a buffer;
    78.         /// </summary>
    79.         /// <typeparam name="T">The type.</typeparam>
    80.         /// <param name="list">The list to add the values to.</param>
    81.         /// <param name="arrayBuffer">The buffer to add from.</param>
    82.         /// <param name="length">The length of the buffer.</param>
    83.         public static unsafe void AddRange<T>(this List<T> list, void* arrayBuffer, int length)
    84.             where T : struct
    85.         {
    86.             var index = list.Count;
    87.             var newLength = index + length;
    88.  
    89.             // Resize our list if we require
    90.             if (list.Capacity < newLength)
    91.             {
    92.                 list.Capacity = newLength;
    93.             }
    94.  
    95.             var items = NoAllocHelpers.ExtractArrayFromListT(list);
    96.             var size = UnsafeUtility.SizeOf<T>();
    97.  
    98.             // Get the pointer to the end of the list
    99.             var bufferStart = (IntPtr)UnsafeUtility.AddressOf(ref items[0]);
    100.             var buffer = (byte*)(bufferStart + (size * index));
    101.  
    102.             UnsafeUtility.MemCpy(buffer, arrayBuffer, length * (long)size);
    103.  
    104.             NoAllocHelpers.ResizeList(list, newLength);
    105.         }
    106.     }
    107. }
    And my mesh setter just becomes this

    Code (CSharp):
    1.         private void SetMesh(
    2.             Mesh mesh,
    3.             DynamicBuffer<Vector3> vertices,
    4.             DynamicBuffer<Vector3> uvs,
    5.             DynamicBuffer<Vector3> normals,
    6.             DynamicBuffer<int> triangles)
    7.         {
    8.             mesh.Clear();
    9.  
    10.             if (vertices.Length == 0)
    11.             {
    12.                 return;
    13.             }
    14.  
    15.             this.verticesList.AddRange(vertices);
    16.             this.uvsList.AddRange(uvs);
    17.             this.normalsList.AddRange(normals);
    18.             this.trianglesList.AddRange(triangles);
    19.  
    20.             mesh.SetVertices(this.verticesList);
    21.             mesh.SetNormals(this.normalsList);
    22.             mesh.SetUVs(0, this.uvsList);
    23.             mesh.SetTriangles(this.trianglesList, 0);
    24.  
    25.             this.verticesList.Clear();
    26.             this.normalsList.Clear();
    27.             this.uvsList.Clear();
    28.             this.trianglesList.Clear();
    29.         }
    Much cleaner.
     
    Last edited: Dec 4, 2018
    tiggus, The5, Ivan-Pestrikov and 2 others like this.
  17. Ivan-Pestrikov

    Ivan-Pestrikov

    Joined:
    Aug 8, 2014
    Posts:
    12
    An excellent solution. How do you access the NoAllocHelpers class btw? It's internal if I'm not mistaken.
     
  18. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    1,722
    Last edited: Dec 24, 2018
    Ivan-Pestrikov likes this.
  19. tiggus

    tiggus

    Joined:
    Sep 2, 2010
    Posts:
    1,235
    This is brilliant, thank you.