1
\$\begingroup\$

I'm trying to make a system where the player can do a RTS-like drag-to-select of tiles in a box to mark trees to be chopped or ores to be mined.

I'm testing this system on a large scale, possibly selecting 300x300 or up to 500x500 tiles per frame. For this, I thought jobs + burst would be a good idea. Every time the cursor moves, I check this if this large array of tiles is inside the bounds of the map (a 100x100 rectangle), but it's worth noting this system might be doing other checks as well.

However, using Unity's profiler (deep profile) I don't even use the returned array for anything and I'm getting pretty awful results:

image

It seems like the actual jobs themselves are taking up a tiny fraction of the time (tiny green columns inside the marked red area) but something else on the main thread is bottlenecking it. Here's some of the popups that occur when I click on some of the huge blue bars in the main thread:

enter image description here enter image description here enter image description here enter image description here enter image description here enter image description here

Am I bottlenecking the main thread by initially filling out the NativeArray on the mainthread before even starting the jobs?

Here's the code for the main thread method:

private void UpdateBoxSelection()
{
    Vector3Int newEndNode = Map.GridPosFromMousePos();

    if (newEndNode == EndNode) { return; }

    SelectedNodes.Clear();
    EndNode = newEndNode;

    var lowerLeftTile = new Vector3(Mathf.Min(StartNode.x, EndNode.x), Mathf.Min(StartNode.y, EndNode.y));
    var upperRightTile = new Vector3(Mathf.Max(StartNode.x, EndNode.x), Mathf.Max(StartNode.y, EndNode.y));

    int howManyToCheck = (Mathf.Abs(EndNode.x - StartNode.x) + 1) * (Mathf.Abs(EndNode.y - StartNode.y) + 1);

    var uncheckedNodes = new NativeArray<float3>(howManyToCheck, Allocator.TempJob);

    int index = 0;

    for (int x = (int) lowerLeftTile.x; x <= upperRightTile.x; x++)
    {
        for (int y = (int) lowerLeftTile.y; y <= upperRightTile.y; y++)
        {
            uncheckedNodes[index] = new float3(x, y, 0);
            index++;
        }
    }

    var job = new ReallyToughParallelJob
    {
        UncheckedNodes = uncheckedNodes,
        xMin = Map.Instance.Rect.xMin,
        xMax = Map.Instance.Rect.xMax,
        yMin = Map.Instance.Rect.yMin,
        yMax = Map.Instance.Rect.yMax
    };

    JobHandle handle = job.Schedule(howManyToCheck, 1000);
    handle.Complete();

    uncheckedNodes.Dispose();
}

And the code for the Job:

[BurstCompile]
public struct ReallyToughParallelJob : IJobParallelFor
{
    public NativeArray<float3> UncheckedNodes;

    [ReadOnly] public float xMin;
    [ReadOnly] public float xMax;
    [ReadOnly] public float yMin;
    [ReadOnly] public float yMax;

    public void Execute(int index)
    {
        float3 newNode = UncheckedNodes[index];

        if (Contains(newNode)) { UncheckedNodes[index] = newNode; }
        else { UncheckedNodes[index] = float3.zero; }
    }

    public bool Contains(float3 point)
    {
        return point.x >= (double) xMin && point.x < (double) xMax && point.y >= (double) yMin && point.y < (double) yMax;
    }
}
\$\endgroup\$
1
\$\begingroup\$

Yes, it indeed appears as if the majority of the execution time goes into building the NativeArray with all the tile coordinates, while the job operating on said array only takes a tiny fraction of the computational time.

But that array does not even contain very useful data. It's really just a consecutive list of all the coordinates within a large rectangle. Some things you could try are:

  • Don't parallelize per-tile, parallelize per-row. Create a NativeArray<int> of the row numbers you want to check, and then have the Execute-function perform a for-loop over the tiles from start to end of the row. This still gives you 100 units of work for a 100x100 selection. Still more than enough granularity for letting the engine distribute the load on multiple cores in an efficient manner.
  • Create a permanent NativeArray for all tiles on the map which you create once (Allocator.Persistent). Then use NativeSlices to tell the job system which tiles you want checked.
  • Use another (non-parallel) job to build the UncheckedNodes NativeArray. A burst-compiled job might be able to do that more efficiently than regular C# code. Then schedule the ReallyToughParallelJob with the array-building job as a dependency.
  • (probably the best solution) Don't use the array at all. Replace it with these fields to the job which tell the job the area it's supposed to iterate:
[ReadOnly] public float xStart;
[ReadOnly] public float width;
[ReadOnly] public float yStart;
// height is implied by the iteration count you pass when you schedule the job

And then calculate the tile coordinates of the current tile in the Execute method:

x = index % width + xStart;
y = index / width + yStart;
\$\endgroup\$
4
  • \$\begingroup\$ This sounds exactly like the solution I need! Before I try it out, since the documentation is a bit sparce, would you please explain what NativeSlices are and how to use them in this context? I'm not sure how to combine the last part you wrote about (xStart, width, yStart) with NativeSlices that I haven't heard of before. \$\endgroup\$ Jan 5 at 15:01
  • \$\begingroup\$ @franticabyss A native slice is like a view on a part of a native array. It contains a pointer to the array, a start index and a length of entries. You can then use them like arrays. \$\endgroup\$
    – Philipp
    Jan 5 at 15:04
  • \$\begingroup\$ One more thing. What is the best way (in this case, but also generally) to return data out of the job? In this case, I need to return some sort of data structure with all the tiles (their coordinates - Vector3Int) that have passed the check that they are inside of the map. I tried with NativeList and then calling .Add() but it says it doesn't support parallel writing. I can't use a NativeArray because that requires it's size to be initialized before I know how many checked tiles I'm going to return. \$\endgroup\$ Jan 5 at 15:16
  • \$\begingroup\$ @franticabyss How to return an unknown amount of data from a parallel job has nothing to do anymore with your initial problem of how to optimize performance. You might want to post a new question for this problem. \$\endgroup\$
    – Philipp
    Jan 5 at 15:20

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .