Unity Job System for cellular automata type simulation

Hello everyone,

I am new to unity and unity Job system and wanted to multi thread a falling sand simulation that uses cellular automata like rules.

The idea is to make 16 chunks with 4 running at the same time on different threads to not overlap (Picture bellow). This patern allows to have some space around updated chunks in case a particle has to fly far away (1/4 of the width/height of the screen).

6370182--709020--chunk_partern400x100.png
Backup Image link

I don’t know if this works, just in case :
Imgur: The magic of the Internet

Code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Jobs;
using Unity.Jobs;
using Unity.Collections;
using Unity.Burst;
using Unity.Collections.LowLevel.Unsafe;




public class SandSimulationWithJob : MonoBehaviour
{

    public int brushSize = 0;

    public int width = 512;
    public int height = 512;

    [Range(0, 100)]
    public int cellDensity = 50;


    //public Texture texture;
    public Camera cam;

    public ComputeShader computeShader;
    ComputeBuffer computeBuffer;
    RenderTexture renderTexture;

    public Material material;

    private int kernel;
    private int numOfThreads = 32;

    NativeArray<Cell> nativeCells;




    public struct Cell
    {
        public int cellType;
        public Color cellColor;
        public int updated;  //has been updated this frame
    }


    private void Start()
    {
        InitializeCells();

        computeBuffer = new ComputeBuffer(width * height, 24);

        renderTexture = new RenderTexture(width, height, 24);
        renderTexture.enableRandomWrite = true;
        renderTexture.filterMode = FilterMode.Point;
        renderTexture.useMipMap = false;
        renderTexture.Create();

        kernel = computeShader.FindKernel("CSMain");
    }


    void InitializeCells()
    {
        nativeCells = new NativeArray<Cell>(width * height, Allocator.Persistent);


        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                nativeCells[x + (width * y)] = new Cell { cellType = 0, cellColor = Color.black, updated = 0 };
            }
        }
    }



    private void FixedUpdate()
    {
        for (int x = 0; x < width; x++)  // each frame all particle has their updated reset. I have to change how it's done.
        {
            for (int y = 0; y < height; y++)
            {
                Cell tmp_cell = nativeCells[x + (width * y)];
                nativeCells[x + (width * y)] = new Cell { cellType = tmp_cell.cellType, cellColor = tmp_cell.cellColor, updated = 0 };
            }
        }

        SimpleJobHandle();

        Mouse();

        UpdateComputeShader();
    }


    void SimpleJobHandle()
    {
        int k;
        for (int i = 0; i < width / 2; i += width / 4)
        {
            for (int j = 0; j < height / 2; j += height / 4)
            {
                k = 0;
                NativeArray<JobHandle> jobHandleArray = new NativeArray<JobHandle>(4, Allocator.Temp);    //Doing 4 passes of 4 chunks

                for (int x_ = i; x_ < width; x_ += width / 2)
                {
                    for (int y_ = j; y_ < height; y_ += height / 2)
                    {
                        JobHandle jobHandle = new Cells_ { nativeCells = nativeCells, height = height, width = width, x_ = x_, y_ = y_ }.Schedule();
                        jobHandleArray[k] = jobHandle;
                        k++;
                    }
                }
                JobHandle.CompleteAll(jobHandleArray);
                jobHandleArray.Dispose();
            }
        }
    }


    [BurstCompile]
    public struct Cells_ : IJob
    {
        [NativeDisableContainerSafetyRestriction]
        public NativeArray<Cell> nativeCells;

        public int x_;
        public int y_;
        public int width;
        public int height;

        public void Execute()
        {
            for (int x = x_; x < width / 4 + x_; x++)
            {
                for (int y = y_; y < height / 4 + y_; y++)
                {
                    if (nativeCells[x + (width * y)].cellType == 1 && (y + 1) < height && nativeCells[x + (width * y)].updated == 0) //Sand Simulation
                    {
                        if (nativeCells[x + (width * (y + 1))].cellType == 0)
                        {
                            nativeCells[x + (width * (y + 1))] = new Cell { cellType = 1, cellColor = Color.white, updated = 1 };

                            nativeCells[x + (width * y)] = new Cell { cellType = 0, cellColor = Color.black, updated = 0 };

                        }
                        else if ((x + 1) < width && nativeCells[(x + 1) + (width * (y + 1))].cellType == 0)
                        {
                            nativeCells[(x + 1) + (width * (y + 1))] = new Cell { cellType = 1, cellColor = Color.white, updated = 1 };

                            nativeCells[x + (width * y)] = new Cell { cellType = 0, cellColor = Color.black, updated = 0 };
                        }
                        else if ((x - 1) >= 0 && nativeCells[(x - 1) + (width * (y + 1))].cellType == 0)
                        {
                            nativeCells[(x - 1) + (width * (y + 1))] = new Cell { cellType = 1, cellColor = Color.white, updated = 1 };

                            nativeCells[x + (width * y)] = new Cell { cellType = 0, cellColor = Color.black, updated = 0 };
                        }
                        else if (nativeCells[x + (width * (y + 1))].cellType == 2)                                                     //Swap with water
                        {
                            nativeCells[x + (width * (y + 1))] = new Cell { cellType = 1, cellColor = Color.white, updated = 1 };

                            nativeCells[x + (width * y)] = new Cell { cellType = 2, cellColor = Color.blue, updated = 0 };
                        }
                    }
                }
            }
        }
    }


    void UpdateComputeShader()
    {
        computeBuffer.SetData(nativeCells);
        computeShader.SetInt("width", width);
        computeShader.SetInt("height", height);
        computeShader.SetBuffer(kernel, "Grid", computeBuffer);
        computeShader.SetTexture(kernel, "Result", renderTexture);
        computeShader.Dispatch(kernel, width / numOfThreads, height / numOfThreads, 1);

        material.mainTexture = renderTexture;
    }


    void Mouse()
    {
        RaycastHit hit;
        Ray ray = cam.ScreenPointToRay(Input.mousePosition);

        if (Physics.Raycast(ray, out hit))
        {
            Vector3 pos = ray.GetPoint(hit.distance);
            Vector2 pixelPos;
            pixelPos.x = Mathf.Lerp(width, 0, (pos.x + 5f) / 10f);
            pixelPos.y = Mathf.Lerp(height, 0, (pos.z + 5f) / 10f);

            if (Input.GetMouseButton(0))
            {
                for (int x = -brushSize; x < brushSize + 1; x++)
                {
                    for (int y = -brushSize; y < brushSize + 1; y++)
                    {
                        if ((int)pixelPos.y + y >= 0 && (int)pixelPos.y + y < height && (int)pixelPos.x + x >= 0 && (int)pixelPos.x + x < width)
                        {
                            if (x * x + y * y <= brushSize * brushSize + 1)
                            {
                                if (Input.GetKey(KeyCode.S))
                                {
                                    nativeCells[((int)pixelPos.x + x) + (width * ((int)pixelPos.y + y))] = new Cell { cellType = 1, cellColor = Color.white, updated = 0 };

                                }
                                else if (Input.GetKey(KeyCode.W))
                                {
                                    nativeCells[((int)pixelPos.x + x) + (width * ((int)pixelPos.y + y))] = new Cell { cellType = 2, cellColor = Color.blue, updated = 0 };
                                }
                                else if (Input.GetKey(KeyCode.X))
                                {
                                    nativeCells[((int)pixelPos.x + x) + (width * ((int)pixelPos.y + y))] = new Cell { cellType = 3, cellColor = Color.yellow, updated = 0 };
                                }
                            }
                        }
                    }
                }
            }
            if (Input.GetMouseButton(1))
            {
                for (int x = -brushSize; x < brushSize + 1; x++)
                {
                    for (int y = -brushSize; y < brushSize + 1; y++)
                    {
                        if ((int)pixelPos.y + y >= 0 && (int)pixelPos.y + y < height && (int)pixelPos.x + x >= 0 && (int)pixelPos.x + x < width)
                        {
                            if (x * x + y * y <= brushSize * brushSize + 1)
                            {
                                nativeCells[((int)pixelPos.x + x) + (width * ((int)pixelPos.y + y))] = new Cell { cellType = 0, cellColor = Color.black, updated = 0 };
                            }
                        }
                    }
                }
            }
        }
    }


    void OnDestroy()
    {
        renderTexture.Release();
        computeBuffer.Release();
        nativeCells.Dispose();
    }


}

NB : The compute shader is only for the display here for now.
Compute Shader

// Each #kernel tells which function to compile; you can have many kernels
#pragma kernel CSMain

struct Cell {
    uint cellType;
    float4 cellColor;
    uint updated;
};

uint width;
uint height;


RWTexture2D<float4> Result;
RWStructuredBuffer<Cell> Grid;

[numthreads(32,32,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    Cell cell = Grid[id.x + (width * id.y)];
    Result[id.xy] = cell.cellColor;

}

The fact is : It’s slower than the single threaded version, is it because of all the for loops i have for the chunk structure ? Or am I missing something ?

Thank you if you read through the code !
If you have any suggestion feel free to answer, like I said I’m new to unity and may be doing some weird things !

The Profiler should give you some decent answers: Window → Analysis → Profiler. It is certainly the best place to begin performance investigations.

Thank you for the answer, I’ll look at the Profiler.