Search Unity

Question Advice Regarding Working Within ParallelFor index restrictions.

Discussion in 'C# Job System' started by IronGremlin, Jun 2, 2023.

  1. IronGremlin

    IronGremlin

    Joined:
    Mar 7, 2023
    Posts:
    2
    Hello,

    I'm attempting to use the Job system to parallelize a large-ish (2500x2500 grid cells) water simulation.

    Disclaimer: I am not currently trying to make this thing run in real-time, this is part of a background process during map generation, so my over-arching goal is just to get it running fast enough that players wouldn't feel compelled to go order a pizza while the map generates. I'm currently within an order of magnitude of that performance with a single-threaded solution, and am seeking to milk just a little bit more out of it.

    The simulation code in question is using a simplified 'pressure' simulation wherein each cell calculates how much water it "desires" to move to eligible neighbours, and allocates some portion of that every "tick".


    Because each cell can write to it's neighbours, obviously naively allocating this work would lead to a bunch of write conflicts.

    To work around this, I've attempted the following scheme:

    • Divide each row of the grid into 3 "colors", red, blue, and green - each of these represents a staggered pattern of rows to make kind of a R/W DMZ, proving that no individual red-row could write to any other red-row when processed.
    • Make three sets of job executions - each job execution owns one row, and all jobs of one "color" are completed before moving on to the next "color."

    Unfortunately, the compiler "does not believe me" - since the write index for each job is not the 'work index' for each job, it barfs, and tells me it can't assure there won't be any write conflicts.

    I'd like to be able to solve for this by individually allocating each row as a native array, and have each job index reference 'triplets' of output rows, but since AFAIK I cannot pass NativeArrays around by refference, AFAICT that would force me to re-allocate the entire grid in between each job pass instead of just passing different 'slices' of the same underlying data to each 'color,' which is probably going to end up being more work than just doing the whole thing in one thread.

    It really feels like there should be a way to solve for this, but I just cannot wrap my head around it - does anyone have any advice? I am somewhat new to C# and Unity, and would also find value in advice such as "this is a fundamentally bad strategy and you should not use the job system to solve this problem" - even better would be a hint about what else to investigate, but I'll take what I can get.


    Here's the code:


    Code (CSharp):
    1.  
    2. public class Flooder {
    3. // Other not relevant code
    4. // ...
    5.  
    6.     public void HandleWaterParallel(float[] map, ref Vector4[]waterMap)
    7.     {
    8.         Diags.Stopwatch watch = new Diags.Stopwatch();
    9.         watch.Start();
    10.         if (redCells == null || blueCells == null || greenCells == null)
    11.         {
    12.             primeWorkIndexes();
    13.         }
    14.         NativeArray<float> mapBuffer = new NativeArray<float>(map, Allocator.Persistent);
    15.         NativeArray<Vector4> waterBuffer = new NativeArray<Vector4>(waterMap, Allocator.TempJob);
    16.         NativeArray<int> moore = new NativeArray<int>(moores, Allocator.Persistent);
    17.         NativeArray<int> green = new NativeArray<int>(greenCells, Allocator.Persistent);
    18.         NativeArray<int> red = new NativeArray<int>(redCells, Allocator.Persistent);
    19.         NativeArray<int> blue = new NativeArray<int>(blueCells, Allocator.Persistent);
    20.  
    21.         ScanLineFlood greenFloodJob = new ScanLineFlood(mapBuffer, waterBuffer, green, moore, width);
    22.         JobHandle greenHandle = greenFloodJob.Schedule(green.Length, new JobHandle());
    23.         ScanLineFlood blueFloodJob = new ScanLineFlood(mapBuffer, waterBuffer, blue, moore, width);
    24.         JobHandle blueHandle = blueFloodJob.Schedule(blue.Length, greenHandle);
    25.         ScanLineFlood redFloodJob = new ScanLineFlood(mapBuffer, waterBuffer, red, moore, width);
    26.         JobHandle redHandle = redFloodJob.Schedule(red.Length, blueHandle);
    27.         redHandle.Complete();
    28.         waterBuffer.CopyTo(waterMap);
    29.         waterBuffer.Dispose();
    30.         mapBuffer.Dispose();
    31.         moore.Dispose();
    32.         green.Dispose();
    33.         red.Dispose();
    34.         blue.Dispose();
    35.         watch.Stop();
    36.         Debug.Log($"Parallel Flood elapsed : {watch.Elapsed}");
    37.     }
    38.    
    39.     void primeWorkIndexes()
    40.     {
    41.         List<int> r = new List<int>();
    42.         List<int> g = new List<int>();
    43.         List<int> b = new List<int>();
    44.         for (int i = 0; i < width; i++)
    45.         {
    46.             if (i % 3 == 0) r.Add(i);
    47.             if (i != 0 && (i + 1) % 3 == 0) g.Add(i);
    48.             if (i != 0 && (i + 2) % 3 == 0) b.Add(i);
    49.         }
    50.         redCells = new int[r.Count];
    51.         greenCells = new int[g.Count];
    52.         blueCells = new int[b.Count];
    53.         for (int i = 0; i < r.Count; i++)
    54.         {
    55.             redCells[i] = r[i];
    56.         }
    57.         for (int i = 0; i < g.Count; i++)
    58.         {
    59.             greenCells[i] = g[i];
    60.         }
    61.         for (int i = 0; i < b.Count; i++)
    62.         {
    63.             blueCells[i] = b[i];
    64.         }
    65.     }
    66.     int[] redCells;
    67.     int[] greenCells;
    68.     int[] blueCells;
    69.  
    70.     internal struct ScanLineFlood : IJobFor
    71.  
    72.     {
    73.         [ReadOnly] NativeArray<float> map;
    74.         NativeArray<Vector4> waterMap;
    75.         [ReadOnly] NativeArray<int> idxs;
    76.         [ReadOnly] NativeArray<int> moores;
    77.         int width;
    78.  
    79.  
    80.         public ScanLineFlood(NativeArray<float> map,
    81.             NativeArray<Vector4> riverMap,
    82.             NativeArray<int> idxs,
    83.             NativeArray<int> moores,
    84.             int width)
    85.         {
    86.             this.map = map;
    87.             this.waterMap = riverMap;
    88.             this.idxs = idxs;
    89.             this.moores = moores;
    90.             this.width = width;
    91.         }
    92.  
    93.         public void Execute(int yIndex)
    94.         {
    95.             int y = idxs[yIndex];
    96.             for (int x = 0; x < width; x++)
    97.             {
    98.                 int cellIndex = y * width + x;
    99.                 float origVolume = waterMap[cellIndex].w;
    100.  
    101.                 if (origVolume <= 0.1f) continue;
    102.                 var originCell = new Cell(cellIndex, map[cellIndex], origVolume);
    103.                 Cell[] neighbourhood = new Cell[8];
    104.                 float[] desires = new float[8];
    105.                 float sumD = 0f;
    106.                 int desirable = 0;
    107.                 for (int i = 0; i < 8; i++)
    108.                 {
    109.                     int ni = cellIndex + moores[i];
    110.                     if (validNeighbour(cellIndex, ni))
    111.                     {
    112.                         var cell = new Cell(ni, map[ni], waterMap[ni].w);
    113.                         neighbourhood[i] = cell;
    114.                         float desire = flowDesire(originCell, waterLevel(cell));
    115.                         desires[i] = desire;
    116.                         if (desire > 0)
    117.                         {
    118.                             sumD += desire;
    119.                             desirable++;
    120.                         }
    121.  
    122.                     }
    123.                     else
    124.                     {
    125.                         neighbourhood[i] = Cell.bogus();
    126.                         desires[i] = float.NegativeInfinity;
    127.                     }
    128.                 }
    129.                 if (desirable == 0 || sumD <= 0.1f)
    130.                 {
    131.                     continue;
    132.                 }
    133.  
    134.                 for (int i = 0; i < 8; i++)
    135.                 {
    136.                     if (desires[i] > 0)
    137.                     {
    138.  
    139.  
    140.                         float allocation = (desires[i] / sumD) * desires[i];
    141.  
    142.                         allocation = Mathf.Clamp(allocation, 0, originCell.waterVolume);
    143.                         originCell.waterVolume -= allocation;
    144.                         Cell n = neighbourhood[i];
    145.                         n.waterVolume += allocation;
    146.                         neighbourhood[i] = n;
    147.                         if (originCell.waterVolume == 0) break;
    148.                     }
    149.                 }
    150.                 for (int i = 0; i < neighbourhood.Length; i++)
    151.                 {
    152.                     Cell n = neighbourhood[i];
    153.  
    154.                     if (n.discarded) continue;
    155.                     Vector4 nv = waterMap[n.origidx];
    156.                     nv.w = n.waterVolume;
    157.                     waterMap[n.origidx] = nv;
    158.                 }
    159.  
    160.                 Vector4 ov = waterMap[cellIndex];
    161.                 ov.w = originCell.waterVolume;
    162.                 waterMap[cellIndex] = ov;
    163.             }
    164.            
    165.         }
    166.         bool inBounds(int ci) => ci >= 0 && ci < width * width;
    167.         bool validNeighbour(int oi, int ni)
    168.         {
    169.             int ox = oi % width,
    170.                 oy = oi / width,
    171.                 nx = ni % width,
    172.                 ny = ni / width;
    173.             return inBounds(ni) && Mathf.Abs(ox - nx) == 1 && Mathf.Abs(oy - ny) == 1;
    174.  
    175.         }
    176.         float waterLevel(Cell c) => c.height + (c.waterVolume);
    177.         float flowDesire(Cell c, float targetHeight) => waterLevel(c) - targetHeight;
    178.     }
    179.  
    180. }
     
  2. IronGremlin

    IronGremlin

    Joined:
    Mar 7, 2023
    Posts:
    2
    OK, cool, so not that this is -documented- anywhere that I can find, but I randomly stumbled across another forum post while researching a totally different topic.

    This is what "NativeDisableParallelForRestriction" is for -
    By applying this to the 'waterMap' array property on the job, it stops checking to ensure that the 'workIndex' matches the index in the output array, and the code works as written.
     
    Yoreki likes this.