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.
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.)
[BurstCompile]
private struct GenerateMeshJob : IJob
{
// ...
// basically a wrapper for a int pointer, from https://github.com/keijiro/Firefly
public NativeCounter.Concurrent Counter;
// Pointers to the start of an array
[NativeDisableUnsafePtrRestriction] public void* Vertices;
public void Execute()
{
// ...
UnsafeUtility.WriteArrayElement(Vertices, vertCount + n, vertices);
// ...
}
}
// Job creation
var generateMeshJob = new GenerateMeshJob
{
Vertices = UnsafeUtility.AddressOf(ref vertices[0]),
}
.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).
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.
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.
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.
public static class NativeArrayExtensions
{
public static unsafe void CopyToFast<T>(
this NativeArray<T> nativeArray,
T[] array)
where T : struct
{
if (array == null)
{
throw new NullReferenceException(nameof(array) + " is null");
}
int nativeArrayLength = nativeArray.Length;
if (array.Length < nativeArrayLength)
{
throw new IndexOutOfRangeException(
nameof(array) + " is shorter than " + nameof(nativeArray));
}
int byteLength = nativeArray.Length * Marshal.SizeOf(default(T));
void* managedBuffer = UnsafeUtility.AddressOf(ref array[0]);
void* nativeBuffer = nativeArray.GetUnsafePtr();
Buffer.MemoryCopy(nativeBuffer, managedBuffer, byteLength, byteLength);
}
}
EDIT:
I did some speed testing and replaced the buffer.MemoryCopy with this on windows platform
#if PLATFORM_STANDALONE_WIN
[DllImport("msvcrt.dll", EntryPoint = "memcpy")]
public static extern void CopyMemory(IntPtr pDest, IntPtr pSrc, int length);
#endif
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.
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.
var array = new int[30];
var unsafeNativeArray = new UnsafeNativeArray<int>(array);
var job = new Job
{
UnsafeNativeArray = unsafeNativeArray; // use UnsafeNativeArray just like a NativeArray
}.Schedule();
job.Complete(); // make sure job is complete before using array
// 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.
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.
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.
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.AddRange(DynamicBuffer)
namespace BovineLabs.Common.Native
{
using System;
using System.Collections.Generic;
using BovineLabs.Common.Utility;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Entities;
/// <summary>
/// Extensions for Native Containers.
/// </summary>
public static class Extensions
{
/// <summary>
/// Adds a native version of <see cref="List{T}.AddRange(IEnumerable{T})"/>.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="list">The <see cref="List{T}"/> to add to.</param>
/// <param name="array">The native array to add to the list.</param>
public static unsafe void AddRange<T>(this List<T> list, NativeArray<T> array)
where T : struct
{
AddRange(list, array, array.Length);
}
/// <summary>
/// Adds a native version of <see cref="List{T}.AddRange(IEnumerable{T})"/>.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="list">The <see cref="List{T}"/> to add to.</param>
/// <param name="array">The array to add to the list.</param>
/// <param name="length">The length of the array to add to the list.</param>
public static unsafe void AddRange<T>(this List<T> list, NativeArray<T> array, int length)
where T : struct
{
list.AddRange(array.GetUnsafeReadOnlyPtr(), length);
}
/// <summary>
/// Adds a native version of <see cref="List{T}.AddRange(IEnumerable{T})"/>.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="list">The <see cref="List{T}"/> to add to.</param>
/// <param name="nativeList">The native list to add to the list.</param>
public static unsafe void AddRange<T>(this List<T> list, NativeList<T> nativeList)
where T : struct
{
list.AddRange(nativeList.GetUnsafePtr(), nativeList.Length);
}
/// <summary>
/// Adds a native version of <see cref="List{T}.AddRange(IEnumerable{T})"/>.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="list">The <see cref="List{T}"/> to add to.</param>
/// <param name="nativeSlice">The array to add to the list.</param>
public static unsafe void AddRange<T>(this List<T> list, NativeSlice<T> nativeSlice)
where T : struct
{
list.AddRange(nativeSlice.GetUnsafeReadOnlyPtr(), nativeSlice.Length);
}
/// <summary>
/// Adds a native version of <see cref="List{T}.AddRange(IEnumerable{T})"/>.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="list">The <see cref="List{T}"/> to add to.</param>
/// <param name="dynamicBuffer">The dynamic buffer to add to the list.</param>
public static unsafe void AddRange<T>(this List<T> list, DynamicBuffer<T> dynamicBuffer)
where T : struct
{
list.AddRange(dynamicBuffer.GetUnsafePtr(), dynamicBuffer.Length);
}
/// <summary>
/// Adds a range of values to a list using a buffer;
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="list">The list to add the values to.</param>
/// <param name="arrayBuffer">The buffer to add from.</param>
/// <param name="length">The length of the buffer.</param>
public static unsafe void AddRange<T>(this List<T> list, void* arrayBuffer, int length)
where T : struct
{
var index = list.Count;
var newLength = index + length;
// Resize our list if we require
if (list.Capacity < newLength)
{
list.Capacity = newLength;
}
var items = NoAllocHelpers.ExtractArrayFromListT(list);
var size = UnsafeUtility.SizeOf<T>();
// Get the pointer to the end of the list
var bufferStart = (IntPtr)UnsafeUtility.AddressOf(ref items[0]);
var buffer = (byte*)(bufferStart + (size * index));
UnsafeUtility.MemCpy(buffer, arrayBuffer, length * (long)size);
NoAllocHelpers.ResizeList(list, newLength);
}
}
}