diff --git a/JacksonDunstanNativeCollections/NativePerJobThreadIntPtrs.cs b/JacksonDunstanNativeCollections/NativePerJobThreadIntPtrs.cs new file mode 100644 index 0000000..7bb5903 --- /dev/null +++ b/JacksonDunstanNativeCollections/NativePerJobThreadIntPtrs.cs @@ -0,0 +1,449 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) Jackson Dunstan. See LICENSE.txt. +// +//----------------------------------------------------------------------- + +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using Unity.Burst; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Jobs.LowLevel.Unsafe; + +namespace JacksonDunstan.NativeCollections +{ + /// + /// A pointer to an int array stored in native (i.e. unmanaged) memory. One + /// integer array is stored for each of the maximum number of job threads. + /// As of Unity 2018.2, this results in 8 KB of memory usage for every 16 + /// integers. The advantage over is that all + /// operations on are faster due to not being + /// atomic. The resulting array ints are collected with a loop. This is + /// therefore a good option when most usage is via + /// and memory usage is not a concern. + /// + [NativeContainer] + [NativeContainerSupportsDeallocateOnJobCompletion] + [DebuggerTypeProxy(typeof(NativePerJobThreadIntPtrsDebugView))] + [DebuggerDisplay("Value = NativeArray")] + [StructLayout(LayoutKind.Sequential)] + public unsafe struct NativePerJobThreadIntPtrs : IDisposable + { + /// + /// An atomic write-only version of the object suitable for use in a + /// ParallelFor job + /// + [NativeContainer] + [NativeContainerIsAtomicWriteOnly] + public struct Parallel + { + + /// + /// The number of integers stored in the array. + /// + public readonly int Length; + + /// + /// Pointers to the integer values in native memory + /// + [NativeDisableUnsafePtrRestriction] + internal int* m_Buffer; + + /// + /// Thread index of the job using this object. This is set by Unity + /// and must have this exact name and type. + /// + [NativeSetThreadIndex] + internal int m_ThreadIndex; + +#if ENABLE_UNITY_COLLECTIONS_CHECKS + /// + /// A handle to information about what operations can be safely + /// performed on the object at any given time. + /// + internal AtomicSafetyHandle m_Safety; + + /// + /// Create a parallel version of the object + /// + /// + /// + /// The number of integers stored in the array + /// + /// + /// + /// Pointer to the value + /// + /// + /// + /// Atomic safety handle for the object + /// + internal Parallel(int length, int* value, AtomicSafetyHandle safety) + { + Length = length; + m_Buffer = value; + m_ThreadIndex = 0; + m_Safety = safety; + } +#else + /// + /// Create a parallel version of the object + /// + /// + /// + /// The number of integers stored in the array + /// + /// + /// + /// Pointer to the value + /// + internal Parallel(int length, int* value) + { + Length = length; + m_Buffer = value; + m_ThreadIndex = 0; + } +#endif + + /// + /// Increment the stored value + /// + [WriteAccessRequired] + public void Increment(int index) + { + RequireWriteAccess(); +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (index < 0 || index >= Length) throw new ArgumentException("Index must be between 0 and Length - 1.", nameof(index)); +#endif + m_Buffer[IntsPerCacheLine * m_ThreadIndex + index]++; + } + + /// + /// Decrement the stored value + /// + [WriteAccessRequired] + public void Decrement(int index) + { + RequireWriteAccess(); +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (index < 0 || index >= Length) throw new ArgumentException("Index must be between 0 and Length - 1.", nameof(index)); +#endif + m_Buffer[IntsPerCacheLine * m_ThreadIndex + index]--; + } + + /// + /// Add to the stored value + /// + /// + /// + /// Value to add. Use negative values for subtraction. + /// + [WriteAccessRequired] + public void Add(int index, int value) + { + RequireWriteAccess(); +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (index < 0 || index >= Length) throw new ArgumentException("Index must be between 0 and Length - 1.", nameof(index)); +#endif + m_Buffer[IntsPerCacheLine * m_ThreadIndex + index] += value; + } + + /// + /// Throw an exception if the object isn't writable + /// + [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] + private void RequireWriteAccess() + { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + AtomicSafetyHandle.CheckWriteAndThrow(m_Safety); +#endif + } + } + + /// + /// The number of integers stored in the array. + /// + public readonly int Length; + + /// + /// Pointers to the integer values in native memory. Must be named + /// exactly this way to allow for + /// [NativeContainerSupportsDeallocateOnJobCompletion] + /// + [NativeDisableUnsafePtrRestriction] + internal int* m_Buffer; + + /// + /// Allocator used to create the backing memory + /// + /// This field must be named this way to comply with + /// [NativeContainerSupportsDeallocateOnJobCompletion] + /// + internal Allocator m_AllocatorLabel; + + // These fields are all required when safety checks are enabled +#if ENABLE_UNITY_COLLECTIONS_CHECKS + /// + /// A handle to information about what operations can be safely + /// performed on the object at any given time. + /// + private AtomicSafetyHandle m_Safety; + + /// + /// A handle that can be used to tell if the object has been disposed + /// yet or not, which allows for error-checking double disposal. + /// + [NativeSetClassTypeToNullOnSchedule] + private DisposeSentinel m_DisposeSentinel; +#endif + + /// + /// The number of integers that fit into a CPU cache line + /// + private const int IntsPerCacheLine = JobsUtility.CacheLineSize / sizeof(int); + + /// + /// The number of integers that fit into a CPU cache line + /// + private readonly int NumCacheLines; + + /// + /// Allocate memory and set the initial value + /// + /// + /// + /// The number of logical integers to allocate. Initial value is 0 for + /// all array elements. + /// + /// + /// + /// Allocator to allocate and deallocate with. Must be valid. + /// + public NativePerJobThreadIntPtrs(int length, Allocator allocator) + { + // Require a valid allocator + if (!UnsafeUtility.IsValidAllocator(allocator)) + { + throw new ArgumentException( + "Allocator must be Temp, TempJob or Persistent", + "allocator"); + } + + // Need a multiple of the number of cache lines + Length = length; + NumCacheLines = (int)Math.Ceiling((double)length / IntsPerCacheLine); + + // Allocate the memory for the values + int bufferSize = JobsUtility.CacheLineSize * NumCacheLines * JobsUtility.MaxJobThreadCount; + m_Buffer = (int*)UnsafeUtility.Malloc( + bufferSize, + UnsafeUtility.AlignOf(), + allocator); + UnsafeUtility.MemClear(m_Buffer, bufferSize); + + // Store the allocator to use when deallocating + m_AllocatorLabel = allocator; + + // Create the dispose sentinel +#if ENABLE_UNITY_COLLECTIONS_CHECKS +#if UNITY_2018_3_OR_NEWER + DisposeSentinel.Create(out m_Safety, out m_DisposeSentinel, 0, allocator); +#else + DisposeSentinel.Create(out m_Safety, out m_DisposeSentinel, 0); +#endif +#endif + } + + /// + /// Get or set the contained value at the provided index + /// + /// This operation requires read access to the node for 'get' and write + /// access to the node for 'set'. + /// + /// + /// + /// The contained value at the provided index. + /// + public int this[int index] + { + get + { + RequireReadAccess(); +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (index < 0 || index >= Length) throw new ArgumentException("Index must be between 0 and Length - 1.", nameof(index)); +#endif + int value = 0; + for (int i = 0; i < JobsUtility.MaxJobThreadCount; ++i) + { + value += m_Buffer[IntsPerCacheLine * i + index]; + } + return value; + } + + [WriteAccessRequired] + set + { + RequireWriteAccess(); +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (index < 0 || index >= Length) throw new ArgumentException("Index must be between 0 and Length - 1.", nameof(index)); +#endif + *(m_Buffer + index) = value; + for (int i = 1; i < JobsUtility.MaxJobThreadCount; ++i) + { + m_Buffer[IntsPerCacheLine * i + index] = 0; + } + } + } + + /// + /// Get a version of this object suitable for use in a ParallelFor job + /// + /// + /// + /// A version of this object suitable for use in a ParallelFor job + /// + public Parallel GetParallel() + { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + Parallel parallel = new Parallel(Length, m_Buffer, m_Safety); + AtomicSafetyHandle.UseSecondaryVersion(ref parallel.m_Safety); +#else + Parallel parallel = new Parallel(Length, m_Buffer); +#endif + return parallel; + } + + /// + /// Check if the underlying unmanaged memory has been created and not + /// freed via a call to . + /// + /// This operation has no access requirements. + /// + /// This operation is O(1). + /// + /// + /// + /// Initially true when a non-default constructor is called but + /// initially false when the default constructor is used. After + /// is called, this becomes false. Note that + /// calling on one copy of this object doesn't + /// result in this becoming false for all copies if it was true before. + /// This property should not be used to check whether the object + /// is usable, only to check whether it was ever usable. + /// + public bool IsCreated + { + get + { + return m_Buffer != null; + } + } + + /// + /// Release the object's unmanaged memory. Do not use it after this. Do + /// not call on copies of the object either. + /// + /// This operation requires write access. + /// + /// This complexity of this operation is O(1) plus the allocator's + /// deallocation complexity. + /// + [WriteAccessRequired] + public void Dispose() + { + RequireWriteAccess(); + +// Make sure we're not double-disposing +#if ENABLE_UNITY_COLLECTIONS_CHECKS +#if UNITY_2018_3_OR_NEWER + DisposeSentinel.Dispose(ref m_Safety, ref m_DisposeSentinel); +#else + DisposeSentinel.Dispose(m_Safety, ref m_DisposeSentinel); +#endif +#endif + + UnsafeUtility.Free(m_Buffer, m_AllocatorLabel); + m_Buffer = null; + } + + /// + /// Set whether both read and write access should be allowed. This is + /// used for automated testing purposes only. + /// + /// + /// + /// If both read and write access should be allowed + /// + [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] + [BurstDiscard] + public void TestUseOnlySetAllowReadAndWriteAccess( + bool allowReadOrWriteAccess) + { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + AtomicSafetyHandle.SetAllowReadOrWriteAccess( + m_Safety, + allowReadOrWriteAccess); +#endif + } + + /// + /// Throw an exception if the object isn't readable + /// + [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] + private void RequireReadAccess() + { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + AtomicSafetyHandle.CheckReadAndThrow(m_Safety); +#endif + } + + /// + /// Throw an exception if the object isn't writable + /// + [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] + private void RequireWriteAccess() + { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + AtomicSafetyHandle.CheckWriteAndThrow(m_Safety); +#endif + } + } + + /// + /// Provides a debugger view of . + /// + internal sealed class NativePerJobThreadIntPtrsDebugView + { + /// + /// The object to provide a debugger view for + /// + private NativePerJobThreadIntPtrs m_Ptrs; + + /// + /// Create the debugger view + /// + /// + /// + /// The object to provide a debugger view for + /// + public NativePerJobThreadIntPtrsDebugView(NativePerJobThreadIntPtrs ptrs) + { + m_Ptrs = ptrs; + } + + /// + /// Get the elements of the array as a managed array + /// + public int[] Items + { + get + { + int[] arr = new int[m_Ptrs.Length]; + for (int i = 0; i < arr.Length; i++) arr[i] = m_Ptrs[i]; + return arr; + } + } + } +} diff --git a/JacksonDunstanNativeCollections/NativePerJobThreadIntPtrs.cs.meta b/JacksonDunstanNativeCollections/NativePerJobThreadIntPtrs.cs.meta new file mode 100644 index 0000000..f68065a --- /dev/null +++ b/JacksonDunstanNativeCollections/NativePerJobThreadIntPtrs.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 87e73f6ca4f7441be9b9ea7916d081d5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: