Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

How to create a struct for the Unity Job System (with Burst) that contains a collection?

Discussion in 'C# Job System' started by John_Leorid, Sep 6, 2019.

  1. John_Leorid

    John_Leorid

    Joined:
    Nov 5, 2012
    Posts:
    646
    This should be pretty basic, I have created some structs to feed my native arrays which are then passed to a IJobParallelFor and I want those structs to contain a collection.

    This is a basic draft of a physics engine and I want to create a physical chain.
    The chain is made out of Points (called Elements) and constraints (called Lines).

    Every Line is connected to two Elements.
    As the chain can have sub-chains, every element knows of the lines it is connected to (minium 1).

    So I need a collection inside the Element struct as every element can be connect to a different amount of Lines.
    Something like

    Code (CSharp):
    1. public struct Element
    2. {
    3.     public List<int> connectedLineIndexes;
    4. }
    5.  
    6. public struct Line
    7. {
    8.     int elementIndexA;
    9.     int elementIndexB;
    10. }
    11.  
    List is not valid, array is not valid, no reference type is valid.
    Then I could use NativeContainers, right? NativeArray<int> connectedLineIndexes;

    But native Containers must be disposed to avoid memory leaks.
    Now my Element is a struct, so data will be copied every time I pass it to a method or read it from another array.
    Do I have to call connectedLineIndexes.Dispose(); every time the struct gets copied?
    Do I have to call connectedLineIndexes.Dispose(); when my application quits?
    Do I have to call it at all?
    Do I have to mark it as [ReadOnly] inside the struct?

    Is there another way to have collections inside structs? (they are used as ReadOnly arrays inside the job, I won't write to them)

    Edit:
    By using a NativeArray<int> in my struct I get the following Error Message:
    Code (CSharp):
    1. InvalidOperationException: PAD.Verlet.VerletSolverJobSystem+Element_S used in NativeArray<PAD.Verlet.VerletSolverJobSystem+Element_S> must be unmanaged (contain no managed types).
     
    Last edited: Sep 6, 2019
  2. Neriad

    Neriad

    Joined:
    Feb 12, 2016
    Posts:
    125
    I don't know if this is the answer to your question but, I think you are missing the interface part.

    In the job system, structs must implement IJob interfaces, in your case, IJobParallelFor, and the code to execute within the job in the function Execute(), Execute(int Index) for IJobParallelFor.
     
  3. Yoreki

    Yoreki

    Joined:
    Apr 10, 2019
    Posts:
    2,605
    The short answer is: You cant. Nesting collections into structs would be the same as allowing native lists of native lists, or native arrays of native arrays and so on. Oh how much i've fought with the same problem when i started using Jobs.

    The longer answer: There is no way to do what you want, which has something to do with how the system is implemented to allow for safety and insane performance. The good thing is, that there is always a solution if you change the way you approach the problem. Most of the time the problem can be solved by one of these approaches:
    • If it's something like a multidimensional lookup table, or any other collection of known, unchanging size, then you can flatten it into a one dimensional native array. This should work always when the size (or max size) is known.
    • If it needs to be a little more dynamic, then using NativeMultiHashMaps is the only way, which can map any given element to any other set of elements. This is the closest to List<List>> you will get, but keep in mind that maps dont guarantee to keep the order of insertion. So if it's important to know which element was inserted first or second, you'll have to use a struct to save the element and the insertion index, in order to reconstruct order later.
    Using Jobs often ends up with a less object oriented approach, since it's part of the data oriented technology stack. This takes some time getting used to, but there is always some solution and you'll have great performance.
    I've been in the "this is impossible" mindset often enough since i started using Jobs, but always ended up figuring out some way to construct my data in a data oriented way. Good luck!

    Also, when you need to dispose something depends on the allocator. Temp is automatically disposed when exiting a scope, TempJob needs to be manually disposed within the next 4 frames to allow for slightly longer running jobs, while Persistent can be disposed anytime (or not at all).
    Keep in mind that disposing becomes slower in this order as well. Not really relevant here but i thought i'd throw it in.
     
    John_Leorid likes this.
  4. John_Leorid

    John_Leorid

    Joined:
    Nov 5, 2012
    Posts:
    646
    Yes, it works, somehow. With multi-hash-maps, even if I don't understand to 100% how they work.

    The strange thing for me is, that I could write some kind of "collection" into my struct on my own, for example:


    Code (CSharp):
    1. public struct MyStruct
    2. {
    3.     int a1, a2, a3, a4, a5, a6;
    4.  
    5.     public int GetElement(int element)
    6.     {
    7.         switch (element)
    8.         {
    9.             case 1: return a1;
    10.             case 2: return a2;
    11.             case 3: return a3;
    12.             case 4: return a4;
    13.             case 5: return a5;
    14.             case 6: return a6;
    15.             default: throw new OutOfBoundsException("The key " + element + "is out of bounds");
    16.         }
    17.     }
    18.  
    19.     public void SetElement(int element, int value)
    20.     {
    21.         switch (element)
    22.         {
    23.             case 1: a1 = value;
    24.             case 2: a2 = value;
    25.             case 3: a3 = value;
    26.             case 4: a4 = value;
    27.             case 5: a5 = value;
    28.             case 6: a6 = value;
    29.             default: throw new OutOfBoundsException("The key " + element + "is out of bounds");
    30.         }
    31.     }
    32. }
    IDK if the switch statement uses a "try" as foreach loops do (it really hurts that I can't use foreach loops tbh, I use them really frequently, usually) but it could also be solved with a If/Else - so theoretically, there should be a way to do this memory-wise on the C++ side too..
    Static collections inside Structs that are used in IJobs should definately be possible I think.

    Also I heard about Dynamic Buffers while I was fixing my code until 05:30 AM ... yes, it took a long, long time to find the right way to write it. (more than 7h, yesterday)

    Is there any documentation how to use them properly?

    How about variables inside IJob-Structs?
    public struct MyJob : IJobParallelFor

    if I write an unassigned variable inside the job, will this variable be persitent inside the job? E.G. The job iterates over the 1st element, then the 5th, the 9th and I have a simple temporary int variable inside my job-struct, lets say
    int tempCounter;

    I increase the tempCounter everytime the job runs by 10.
    First Execution, index 1:
    tempCounter will be 0.
    Second Execution, index 5:
    tempCounter will be ? .. will it be 0 or 10?

    I have so many questions about the system and there is almost nothing to read about it. The Docu ist quite empty with very few examples, there are not a lot of questions asked on Unity Answers/Forums, Tutorial Videos only show the basics and I don't want to invest all the time to test all these things by myself, since I want to finish my game someday. ^^
     
  5. Yoreki

    Yoreki

    Joined:
    Apr 10, 2019
    Posts:
    2,605
    I feel you.. as i've had the same experience. I feel rather confident using the system now, but i'd still wish there was more / better documentation on it. Some things are not documented at all. I literally was looking for some job type once and it found 10 results on google...

    In normal IJobs you can use locally declared variables basically like you are used to, since it's are just a single Job on some other thread that is not the mainthread. For IJobParallelFor however, each index could be executed on some other logical core and there is no real way to communicate between them, other than using pointers and making sure it's safely accessed.
    Readonly should be fine, but in order to use the variable safely it basically has to be a native container. Depending on what exactly you need, you may get away with accessing a single-element native int array like a normal integer. For readability and parallel write cases you may want to create a custom native int wrapper type. This website has lots of useful information about the job system, and some examples for custom containers. It's worth a read imo: https://jacksondunstan.com/articles/4940

    I believe dynamic buffers are mainly used in ECS, which i did not work with yet. Meaning i also did not work with dynamic buffers yet. I mainly used Burstjobs as a way of squeezing some ridiculous performance out of mesh generation / alteration algorithms so far. Was not fun at all, but the results are great.

    You can have as much data in your structs as you want (even tho, at some point it becomes arguably bad practice because of the nature of value types). You just cannot create nested containers. I'm not entirely confident on this topic, but it has something to do with efficient memory layout and managed vs unmanaged code.
    Take the following explanation with a grain of salt, but to my knowledge it's as follows:
    In jobs you cant use managed code (which is most of everything, like normal arrays), so they wrote their own native container types from unmanaged code, using pointers and so on. The whole idea behind jobs was to use them as part of the data oriented technology stack (DOTS), which means two features were of utmost priority: making multithreading safe to use and creatng an efficient memory layout (the "data oriented" part).
    Having nested collections (especially lists) conflicts with the memory layout part - possibly safety as well, not sure.
    Instead of loading a single piece of data, with nested collections you would now need to load an entire collection into the cache, while it's not guaranteed that all of it is needed. Preventing loading an overhead like this is the main advantage of data oriented programming over object oriented programming, and is what makes jobs, and even more so burst, increadibly fast.
    So while nested collections should be possible, i doubt we will ever see them since, to my understanding, it beats the entire purpose of the system.

    Hope this helps.
     
    Last edited: Sep 7, 2019
  6. RageEntertainment

    RageEntertainment

    Joined:
    Jan 23, 2017
    Posts:
    3
    Sorry to revive this thread. I also have been struggling to learn Jobs recently and have been using this pattern that I came up with. Please feel free to correct this explanation and tell me why is wrong, I'm sure it is.

    To create a de-facto 2D array I use an indexer (NativeParallelHashMap<int, int>) then I fill up a NativeArray or NativeList with a reference to the amounts of each "type" in the indexer.

    My example:
    Code (CSharp):
    1. for (int i = 0; i < _objectList.Length; i++)
    2.             {
    3.                 for (int j = 0; j < _objectList[i].DensityPerTerrainChunk; j++)
    4.                 {
    5.                     var output = RandomPoint(_bounds[0], _bounds[1]);
    6.                     _placementTest.Add(new float3(output.x, 0f, output.y));
    7.                 }
    8.                 _placementTestIndex.Add(i, (_objectList[i].DensityPerTerrainChunk - 1));
    9.             }
    This is run on the main thread setting up data to be run in a job.
    In this example, I'm iterating over an array of structs, "objectList", with various parameters and references. The "_placementTestIndex" is a NativeParallelHashMap<key, value> (basically a dictionary) that is added to for each of the "i" iterations. The "key" is the "i" value which in this case is each "objectList" type (trees, rocks, grass, etc) and the "value" is the number of "_placementTests that were added.

    This results in a collection of items, in this case float3 coordinates, and a indexer that keeps track of the range of each group of items. So if have I trees from _placementTest index 0 - 199. _placementTestIndex[0] corresponds to number of trees that were added, which is 200. Then if there are 100 rocks from _placementTest index 200 - 300 then you can retrieve each of the groups but knowing from were to start and how far to iterate through the _placementTest list.

    Code (CSharp):
    1. int placementIndexSum = 0;
    2. for (int type = 0; type < _type; type++)
    3. {
    4.      if (type > 0)
    5.             {
    6.                 placementIndexSum += _placementTestIndex[type - 1];
    7.             }
    8.  
    9.      for (int i = placementIndexSum; i< (_placementTestIndex[type] + placementIndexSum); i++)
    10.      {
    11.          //Logic for each item of each type
    12.          //_placementTest[i].doSomething
    13.      }
    14. }
    This is code in the actual job. The first for loop is for each objectType which corresponds to each _placementTestIndex.key. The placementIndex sum keeps track of the each group starting position. In the example above with the rocks, the starting position is an index of 200 and the ending position would be 300 or "_placementTestIndex[type] + placementIndexSum".
    So that second for loop will iterate over each of the appropriate items in the _placementTest list.

    Hopefully this wasn't as clear as mud and I'm still learning the Jobs system so don't take this method as good advise but it did work for me.
     
  7. RageEntertainment

    RageEntertainment

    Joined:
    Jan 23, 2017
    Posts:
    3
    P.S. I'm a idiot. You can basically hack in 2D arrays by using and NativeParallelHashMap<int2, value>. Here is an example:
    Code (CSharp):
    1.  
    2. NativeParallelHashMap<int2, float3x3> _resultPoints = new NativeParallelHashMap<int2, float3x3>(0, Allocator.TempJob);
    3.  
    4. int commandCounter = 0;
    5. for (int i = 0; i < _objectList.Length; i++)
    6. {
    7.          for (int j = 0; j < _resultPoints.Count(); j++)
    8.          {
    9.                  if (_resultPoints.ContainsKey(new int2(i, j)))
    10.                  {
    11.                         commands[commandCounter] = new RaycastCommand(new Vector3(_resultPoints[new int2(i, j)].c0.x, 100f, _resultPoints[new int2(i, j)].c0.y), Vector3.down);
    12.                         commandCounter++;
    13.                    }
    14.                    else
    15.                    {
    16.                         break;
    17.                     }
    18.            }
    19. }
     
    kdchabuk likes this.