Search Unity

Resolved Custom NativeContainer Deferred Job Support

Discussion in 'Entity Component System' started by chadfranklin47, Apr 6, 2022.

  1. chadfranklin47

    chadfranklin47

    Joined:
    Aug 11, 2015
    Posts:
    229
    Hello, I have created a custom NativeContainer that returns the min and max of all the floats added to it inside an IJobParallelForBatch. Once the job is complete I can read from the MinMax property on the main thread, but I would like to pass the NativeContainer into another job as a deferred NativeContainer in order to eliminate a sync point. Is this possible?

    Here is the NativeContainer currently:

    Code (CSharp):
    1. using System;
    2. using System.Runtime.InteropServices;
    3. using Unity.Collections;
    4. using Unity.Mathematics;
    5. using Unity.Jobs.LowLevel.Unsafe;
    6. using Unity.Collections.LowLevel.Unsafe;
    7.  
    8. [StructLayout(LayoutKind.Sequential)]
    9. [NativeContainer]
    10. public unsafe struct NativeMinMaxFloat
    11. {
    12.     // The actual pointer to the allocated sum needs to have restrictions relaxed so jobs can be scheduled with this utility
    13.     [NativeDisableUnsafePtrRestriction]
    14.     private float2* m_minMaxes;
    15.  
    16. #if ENABLE_UNITY_COLLECTIONS_CHECKS
    17.     private AtomicSafetyHandle m_Safety;
    18.  
    19.     // The dispose sentinel tracks memory leaks. It is a managed type so it is cleared to null when scheduling a job
    20.     // The job cannot dispose the container, and no one else can dispose it until the job has run, so it is ok to not pass it along
    21.     // This attribute is required, without it this NativeContainer cannot be passed to a job; since that would give the job access to a managed object
    22.     [NativeSetClassTypeToNullOnSchedule]
    23.     private DisposeSentinel m_DisposeSentinel;
    24. #endif
    25.  
    26.     // Keep track of where the memory for this was allocated
    27.     private readonly Allocator m_AllocatorLabel;
    28.  
    29.     private static readonly int FLOAT2S_PER_CACHE_LINE = JobsUtility.CacheLineSize / sizeof(float2);
    30.  
    31.     public NativeMinMaxFloat(Allocator label)
    32.     {
    33.         // This check is redundant since we are not using generics and know the type ahead of time.
    34.         // It is here as an example of how to check for type correctness for generic types.
    35. #if ENABLE_UNITY_COLLECTIONS_CHECKS
    36.         if (!UnsafeUtility.IsBlittable<float2>())
    37.         {
    38.             throw new ArgumentException(
    39.                 string.Format("{0} used in NativeQueue<{0}> must be blittable", typeof(float2)));
    40.         }
    41. #endif
    42.         this.m_AllocatorLabel = label;
    43.  
    44.         // Allocate native memory
    45.         this.m_minMaxes = (float2*)UnsafeUtility.Malloc(
    46.             UnsafeUtility.SizeOf<float2>() * FLOAT2S_PER_CACHE_LINE * JobsUtility.MaxJobThreadCount, UnsafeUtility.AlignOf<float2>(), label);
    47.  
    48.         // Create a dispose sentinel to track memory leaks. This also creates the AtomicSafetyHandle
    49. #if ENABLE_UNITY_COLLECTIONS_CHECKS
    50.         DisposeSentinel.Create(out this.m_Safety, out this.m_DisposeSentinel, 0, label);
    51. #endif
    52.  
    53.         Clear();
    54.     }
    55.  
    56.     public void Clear()
    57.     {
    58.         // Clear uninitialized data (actually, initialize the data in this case)
    59.         // Verify that the caller has write permission on this data.
    60.         // This is the race condition protection, without these checks the AtomicSafetyHandle is useless
    61. #if ENABLE_UNITY_COLLECTIONS_CHECKS
    62.         AtomicSafetyHandle.CheckWriteAndThrow(this.m_Safety);
    63. #endif
    64.  
    65.         for (int i = 0; i < JobsUtility.MaxJobThreadCount; ++i)
    66.         {
    67.             this.m_minMaxes[FLOAT2S_PER_CACHE_LINE * i] = new float2(float.MaxValue, float.MinValue);
    68.         }
    69.     }
    70.  
    71.     public void Add(float2 minMax)
    72.     {
    73.         // Verify that the caller has write permission on this data.
    74.         // This is the race condition protection, without these checks the AtomicSafetyHandle is useless
    75. #if ENABLE_UNITY_COLLECTIONS_CHECKS
    76.         AtomicSafetyHandle.CheckWriteAndThrow(this.m_Safety);
    77. #endif
    78.  
    79.         float2 currentMinMax = *this.m_minMaxes;
    80.         *this.m_minMaxes = new float2(math.min(minMax.x, currentMinMax.x), math.max(minMax.y, currentMinMax.y));
    81.     }
    82.  
    83.     public void Add(float val)
    84.     {
    85.         // Verify that the caller has write permission on this data.
    86.         // This is the race condition protection, without these checks the AtomicSafetyHandle is useless
    87. #if ENABLE_UNITY_COLLECTIONS_CHECKS
    88.         AtomicSafetyHandle.CheckWriteAndThrow(this.m_Safety);
    89. #endif
    90.  
    91.         float2 currentMinMax = *this.m_minMaxes;
    92.         (*this.m_minMaxes) = new float2(math.min(val, currentMinMax.x), math.max(val, currentMinMax.y));
    93.     }
    94.  
    95.     public float2 MinMax
    96.     {
    97.         get
    98.         {
    99.             // Verify that the caller has read permission on this data.
    100.             // This is the race condition protection, without these checks the AtomicSafetyHandle is useless
    101. #if ENABLE_UNITY_COLLECTIONS_CHECKS
    102.             AtomicSafetyHandle.CheckReadAndThrow(this.m_Safety);
    103. #endif
    104.             float min = float.MaxValue;
    105.             float max = float.MinValue;
    106.             for (int i = 0; i < JobsUtility.MaxJobThreadCount; ++i)
    107.             {
    108.                 float2 currentMinMax = this.m_minMaxes[FLOAT2S_PER_CACHE_LINE * i];
    109.                 min = math.min(min, currentMinMax.x);
    110.                 max = math.max(max, currentMinMax.y);
    111.             }
    112.  
    113.             return new float2(min, max);
    114.         }
    115.     }
    116.  
    117.     public bool IsCreated
    118.     {
    119.         get
    120.         {
    121.             return this.m_minMaxes != null;
    122.         }
    123.     }
    124.  
    125.     public void Dispose()
    126.     {
    127.         // Let the dispose sentinel know that the data has been freed so it does not report any memory leaks
    128. #if ENABLE_UNITY_COLLECTIONS_CHECKS
    129.         DisposeSentinel.Dispose(ref this.m_Safety, ref this.m_DisposeSentinel);
    130. #endif
    131.  
    132.         UnsafeUtility.Free(this.m_minMaxes, this.m_AllocatorLabel);
    133.         this.m_minMaxes = null;
    134.     }
    135.  
    136.     [NativeContainer]
    137.     // This attribute is what makes it possible to use NativeSum.ParallelWriter in a ParallelFor job
    138.     [NativeContainerIsAtomicWriteOnly]
    139.     public struct ParallelWriter
    140.     {
    141.         // Copy of the pointer from the main NativeSum
    142.         [NativeDisableUnsafePtrRestriction]
    143.         private float2* m_dataPointer;
    144.  
    145.         // Copy of the AtomicSafetyHandle from the full NativeCounter. The dispose sentinel is not copied since this inner struct does not own the memory and is not responsible for freeing it.
    146. #if ENABLE_UNITY_COLLECTIONS_CHECKS
    147.         private AtomicSafetyHandle m_Safety;
    148. #endif
    149.  
    150.         // The current worker thread index; it must use this exact name since it is injected
    151.         [NativeSetThreadIndex]
    152.         int m_ThreadIndex;
    153.  
    154.         // This is what makes it possible to assign to NativeCounter.Concurrent from NativeCounter
    155.         public static implicit operator ParallelWriter(NativeMinMaxFloat nativeMinMaxFloat)
    156.         {
    157.             ParallelWriter parallelWriter;
    158. #if ENABLE_UNITY_COLLECTIONS_CHECKS
    159.             AtomicSafetyHandle.CheckWriteAndThrow(nativeMinMaxFloat.m_Safety);
    160.             parallelWriter.m_Safety = nativeMinMaxFloat.m_Safety;
    161.             AtomicSafetyHandle.UseSecondaryVersion(ref parallelWriter.m_Safety);
    162. #endif
    163.  
    164.             parallelWriter.m_dataPointer = nativeMinMaxFloat.m_minMaxes;
    165.             parallelWriter.m_ThreadIndex = 0;
    166.  
    167.             return parallelWriter;
    168.         }
    169.  
    170.         public void Add(float2 minMax)
    171.         {
    172.             // Verify that the caller has write permission on this data.
    173.             // This is the race condition protection, without these checks the AtomicSafetyHandle is useless
    174. #if ENABLE_UNITY_COLLECTIONS_CHECKS
    175.             AtomicSafetyHandle.CheckWriteAndThrow(this.m_Safety);
    176. #endif
    177.  
    178.             float2 currentMinMax = this.m_dataPointer[FLOAT2S_PER_CACHE_LINE * this.m_ThreadIndex];
    179.             this.m_dataPointer[FLOAT2S_PER_CACHE_LINE * this.m_ThreadIndex] = new float2(math.min(minMax.x, currentMinMax.x), math.max(minMax.y, currentMinMax.y));
    180.         }
    181.  
    182.         public void Add(float val)
    183.         {
    184.             // Verify that the caller has write permission on this data.
    185.             // This is the race condition protection, without these checks the AtomicSafetyHandle is useless
    186. #if ENABLE_UNITY_COLLECTIONS_CHECKS
    187.             AtomicSafetyHandle.CheckWriteAndThrow(this.m_Safety);
    188. #endif
    189.  
    190.             float2 currentMinMax = this.m_dataPointer[FLOAT2S_PER_CACHE_LINE * this.m_ThreadIndex];
    191.             this.m_dataPointer[FLOAT2S_PER_CACHE_LINE * this.m_ThreadIndex] = new float2(math.min(val, currentMinMax.x), math.max(val, currentMinMax.y));
    192.         }
    193.     }
    194. }
    195.  
    I'd like to credit these pages as they helped in the creation of this custom NativeContainer:
    https://coffeebraingames.wordpress.com/2021/10/24/some-dots-utilities-nativecounter-and-nativesum/
    https://docs.unity3d.com/Packages/com.unity.jobs@0.8/manual/custom_job_types.html
     
    Last edited: Jun 2, 2022
  2. chadfranklin47

    chadfranklin47

    Joined:
    Aug 11, 2015
    Posts:
    229
    I was able to avoid the sync point by simply getting the MinMax from inside another job and storing the result inside a NativeReference<float2>.

    Here is said job:
    Code (CSharp):
    1. [BurstCompile]
    2. public struct NativeMinMaxFloatResultJob : IJob
    3. {
    4.     public NativeMinMaxFloat nativeMinMaxFloat;
    5.     public NativeReference<float2> minMaxFloatResult;
    6.  
    7.     public NativeMinMaxFloatResultJob(NativeMinMaxFloat nativeMinMaxFloat, NativeReference<float2> minMaxFloatResult)
    8.     {
    9.         this.nativeMinMaxFloat = nativeMinMaxFloat;
    10.         this.minMaxFloatResult = minMaxFloatResult;
    11.     }
    12.  
    13.     public void Execute()
    14.     {
    15.         minMaxFloatResult.Value = nativeMinMaxFloat.MinMax;
    16.     }
    17. }
     
    vectorized-runner likes this.