Skip to content

Instantly share code, notes, and snippets.

@andrew-raphael-lukasik
Last active July 23, 2021 21:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save andrew-raphael-lukasik/09c8a9c29bb5548ea65273653474f8f1 to your computer and use it in GitHub Desktop.
Save andrew-raphael-lukasik/09c8a9c29bb5548ea65273653474f8f1 to your computer and use it in GitHub Desktop.
Unsafe list-like container built on generic pointers T* that can reallocate mid job execution using Allocator.Persistent
// src*: https://gist.github.com/andrew-raphael-lukasik/09c8a9c29bb5548ea65273653474f8f1
using UnityEngine;
using UnityEngine.Assertions;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
public unsafe struct VeryUnsafeList <T> : System.IDisposable where T : unmanaged
{
public T* ptr;
public readonly Allocator allocator;
public int length;
public int capacity;
public int allocatedBytes { get; private set; }
public VeryUnsafeList ( Allocator allocator )
: this( initialCapacity:0 , allocator:allocator ) {}
public VeryUnsafeList ( int initialCapacity , Allocator allocator )
{
Assert.IsFalse( initialCapacity<0 , "invalid capacity" );
Assert.IsFalse( allocator==Allocator.Invalid , "invalid allocator" );
this.length = 0;
this.capacity = initialCapacity;
this.allocator = allocator;
this.ptr = null;
this.allocatedBytes = 0;
this.Resize( newCapacity:initialCapacity );
}
public T this [ int index ]
{
get
{
if( !AssertIndex(index) ) return default(T);
return this.ptr[index];
}
set
{
if( !AssertIndex(index) ) return;
this.ptr[index] = value;
}
}
public static VeryUnsafeList<T>* Factory ( Allocator allocator ) => Factory( initialCapacity:0 , allocator );
public static VeryUnsafeList<T>* Factory ( int initialCapacity , Allocator allocator )
{
VeryUnsafeList<T>* listPtr = (VeryUnsafeList<T>*) UnsafeUtility.Malloc(
size: UnsafeUtility.SizeOf<VeryUnsafeList<T>>() ,
alignment: UnsafeUtility.AlignOf<VeryUnsafeList<T>>() ,
allocator: allocator
);
*listPtr = new VeryUnsafeList<T>( allocator );
return listPtr;
}
public void Add ( T value )
{
if( this.capacity==0 )
{
this.Resize( 1 );
}
if( this.length==this.capacity )
{
int old = this.capacity;
this.Resize( this.capacity * 2 );
}
this.ptr[this.length++] = value;
}
public void Remove ( T value )
{
if( this.length==0 ) return;
for( int i=0 ; i<this.length ; i++ )
if( this.ptr[i].Equals(value) )
this.ptr[i] = this.ptr[--this.length];
}
public void RemoveAt ( int index )
{
if( !AssertIndex(index) ) return;
this.ptr[index] = this.ptr[--this.length];
}
public void Clear () => this.length = 0;
public void Resize ( int newCapacity )
{
int newAllocatedBytes = UnsafeUtility.SizeOf<T>() * newCapacity;
T* newPtr = (T*) UnsafeUtility.Malloc( size:newAllocatedBytes , alignment:UnsafeUtility.AlignOf<T>() , allocator:this.allocator );
if( this.ptr!=null )
{
UnsafeUtility.MemCpy( destination:newPtr , source:this.ptr , size:Mathf.Min(this.allocatedBytes,newAllocatedBytes) );
this.Dispose();
}
this.capacity = newCapacity;
this.ptr = newPtr;
this.allocatedBytes = newAllocatedBytes;
}
public NativeArray<T> ToArray ( Allocator allocator = Allocator.Temp )
{
var nativeArray = new NativeArray<T>( this.length , allocator );
UnsafeUtility.MemCpy( destination:nativeArray.GetUnsafePtr() , source:this.ptr , this.allocatedBytes );
return nativeArray;
}
/// <returns>A NativeArray "view" of the list.</returns>
public NativeArray<T> AsArray ()
{
var nativeArray = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray<T>( this.ptr , this.length , Allocator.None );
#if ENABLE_UNITY_COLLECTIONS_CHECKS
NativeArrayUnsafeUtility.SetAtomicSafetyHandle( ref nativeArray , AtomicSafetyHandle.Create() );
#endif
return nativeArray;
}
public void Dispose ()
{
if( this.ptr!=null )
{
UnsafeUtility.Free( this.ptr , this.allocator );
this.ptr = null;
this.allocatedBytes = 0;
}
}
public override string ToString () => $"{{ {nameof(ptr)}:{(long)ptr} , {nameof(allocator)}:{allocator} , {nameof(length)}:{length} , {nameof(capacity)}:{capacity} , {(nameof(allocatedBytes))}:{allocatedBytes} }}";
public string ToHex ( int elementIndex )
{
var bytes = new byte[ this.allocatedBytes ];
System.Runtime.InteropServices.Marshal.Copy(
source: new System.IntPtr( this.ptr + elementIndex ) ,
destination: bytes ,
startIndex: 0 ,
length: this.allocatedBytes
);
var sb = new System.Text.StringBuilder();
for( int i=0 ; i<bytes.Length ; i++ )
{
byte b = bytes[i];
string hex = System.Convert.ToString(b,16).ToUpper();
if( b>15 ) sb.Append($" {hex}");
else sb.Append($" 0{hex}");
}
return sb.ToString();
}
bool AssertIndex ( int index )
{
if( index<0 || index>=this.length )
{
if( index<0 )
{
Debug.LogError($"Index is negative: (index) {index} < 0");
return false;
}
else if( index>=this.length )
{
Debug.LogError($"Index is larger (or equal) than list length: (index) {index} >= {this.length} (this.length)");
return false;
}
// throw new System.IndexOutOfRangeException();
}
return true;
}
}
@andrew-raphael-lukasik
Copy link
Author

andrew-raphael-lukasik commented Jun 6, 2021

Tester

AdventuresInUnsafeAllocations.cs

using UnityEngine;
using UnityEngine.Assertions;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs;

using NaughtyAttributes;

public unsafe class AdventuresInUnsafeAllocations : MonoBehaviour
{
	[SerializeField] Allocator mainThreadAllocator = Allocator.Persistent;
	[SerializeField] Allocator jobAllocator = Allocator.Temp;

	void OnEnable () => Run();

	[Button("GO")]
	void Run ()
	{
		VeryUnsafeList<int>* dataPtr = (VeryUnsafeList<int>*) UnsafeUtility.Malloc( size:sizeof(VeryUnsafeList<int>) , alignment:4 , allocator:mainThreadAllocator );
		*dataPtr = new VeryUnsafeList<int>( jobAllocator );
		
		Debug.Log($"run started");
		var job = new AllocationsJob{ dataPtr = dataPtr };
		job.Schedule().Complete();

		var data = *dataPtr;
		var text = new System.Text.StringBuilder();
		for( int i=0 ; i<data.length ; i++ )
			text.Append( data.ptr[i] ).Append( i<data.length-1 ? ',' : ' ' );
		Debug.Log($"({data.length}) output data: {{ {text} }}");
		
		data.Dispose();
		UnsafeUtility.Free( dataPtr , mainThreadAllocator );
		Debug.Log($"run completed using {data.allocator} job allocator");
	}
}

public unsafe struct AllocationsJob : IJob
{
	[NativeDisableUnsafePtrRestriction] public VeryUnsafeList<int>* dataPtr;
	public void Execute ()
	{
		(*dataPtr).Add( 999 );
		for( int i=0 ; i<10 ; i++ )
			(*dataPtr).Add( i );
		(*dataPtr).Add( 11 );
		(*dataPtr).Add( -1 );
		(*dataPtr)[11] = (*dataPtr)[12];
		(*dataPtr).Remove( 999 );
		(*dataPtr).RemoveAt( 11 );
	}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment