NativeArray wont Dispose. Why?

I wrote some pathfinding scripts that seems to works just fine in play mode, but throws errors on playmode exit. It says “A Native Collection has not been disposed, resulting in a memory leak”.

So I called Dispose() for the NativeArray in OnDestroy. This time on exit, it gives me the same error, plus one before it saying the array " has been deallocated, it is not allowed to access it".

Is this actually a problem if I just don’t fix it? What problems will it cause? The script seems to work just fine, so what happens if you don’t dispose of a NativeArray exactly, anything bad? Or can I just leave it as is?
Will this prevent unity from making a build? How would this affect a build?

And, finally, how would I fix this?

Here are my scripts. I wrote them following the DOTS A* Pathfinding tutorial from CodeMonkey.

PathNode:

public struct PathNode {
    public int x;
    public int y;
    public int index;
    public int gCost;
    public int hCost;
    public int fCost;
    public bool isWalkable;
    public int cameFromNodeIndex;
    public void CalculateFCost() {
        fCost = gCost + hCost;
    }
    public void SetIsWalkable(bool isWalkable) {
        this.isWalkable = isWalkable;
    }
}

Pathfinding:

using UnityEngine;
using Unity.Mathematics;
using Unity.Collections;
using Unity.Jobs;
using Unity.Burst;
public class Pathfinding : MonoBehaviour {
    public float pathTimeInterval = 1f;
    const int MOVE_STRAIGHT_COST = 10;
    const int MOVE_DIAGONAL_COST = 14;
    Grid grid;
    public int findPathJobCount = 1;


    public int2 startPosition = new int2(0, 0),
                endPosition = new int2(5, 5);
    float intervalTimer;
    void Awake() {
        grid = FindObjectOfType<Grid>();
    }
    void Update() {
        intervalTimer += Time.deltaTime;
        if (intervalTimer >= pathTimeInterval) {
            DoJobShit();
            intervalTimer = 0;
        }
    }
    void DoJobShit() {
        float startTime = Time.realtimeSinceStartup;
        NativeArray<JobHandle> jobHandleArray = new NativeArray<JobHandle>(findPathJobCount, Allocator.TempJob);
        for (int i = 0; i < findPathJobCount; i++) {
            FindPathJob findPathJob = new FindPathJob {
                _startPosition = startPosition,
                _endPosition = endPosition,
                _gridSize = grid.gridSize,
                _pathNodeArray = grid.grid
            };
            if (i == 0) {
                jobHandleArray[i] = findPathJob.Schedule();
            } else {
                jobHandleArray[i] = findPathJob.Schedule(jobHandleArray[i - 1]);
            }
        }
        JobHandle.CompleteAll(jobHandleArray);
        jobHandleArray.Dispose();
        Debug.Log("Time: " + ((Time.realtimeSinceStartup - startTime) * 1000f));
    }
    [BurstCompile]
    struct FindPathJob : IJob {
        public int2 _startPosition, _endPosition, _gridSize;
        public NativeArray<PathNode> _pathNodeArray;
        public void Execute() {
            for (int x = 0; x < _gridSize.x; x++) {
                for (int y = 0; y < _gridSize.y; y++) {
                    PathNode pathNode = new PathNode();
                    pathNode.gCost = int.MaxValue;
                    pathNode.hCost = CalculateDistanceCost(new int2(x, y), _endPosition);
                    pathNode.CalculateFCost();
                }
            }
        
            NativeArray<int2> neighbourOffsetArray = new NativeArray<int2>(8, Allocator.Temp);
            neighbourOffsetArray[0] = new int2(-1, 0); // Left
            neighbourOffsetArray[1] = new int2(+1, 0); // Right
            neighbourOffsetArray[2] = new int2(0, +1); // Up
            neighbourOffsetArray[3] = new int2(0, -1); // Down
            neighbourOffsetArray[4] = new int2(-1, -1); // Left Down
            neighbourOffsetArray[5] = new int2(-1, +1); // Left Up
            neighbourOffsetArray[6] = new int2(+1, -1); // Right Down
            neighbourOffsetArray[7] = new int2(+1, +1); // Right Up
            int endNodeIndex = CalculateIndex(_endPosition.x, _endPosition.y, _gridSize.x);
            PathNode startNode = _pathNodeArray[CalculateIndex(_startPosition.x, _startPosition.y, _gridSize.x)];
            startNode.gCost = 0;
            startNode.CalculateFCost();
            _pathNodeArray[startNode.index] = startNode;
            NativeList<int> openList = new NativeList<int>(Allocator.Temp);
            NativeList<int> closedList = new NativeList<int>(Allocator.Temp);
            openList.Add(startNode.index);
            while (openList.Length > 0) {
                int currentNodeIndex = GetLowestCostFNodeIndex(openList, _pathNodeArray);
                PathNode currentNode = _pathNodeArray[currentNodeIndex];
                if (currentNodeIndex == endNodeIndex) {
                    // Reached our destination!
                    break;
                }
                // Remove current node from Open List
                for (int i = 0; i < openList.Length; i++) {
                    if (openList[i] == currentNodeIndex) {
                        openList.RemoveAtSwapBack(i);
                        break;
                    }
                }
                closedList.Add(currentNodeIndex);
                for (int i = 0; i < neighbourOffsetArray.Length; i++) {
                    int2 neighbourOffset = neighbourOffsetArray[i];
                    int2 neighbourPosition = new int2(currentNode.x + neighbourOffset.x, currentNode.y + neighbourOffset.y);
                    if (!IsPositionInsideGrid(neighbourPosition, _gridSize)) {
                        // Neighbour not valid position
                        continue;
                    }
                    int neighbourNodeIndex = CalculateIndex(neighbourPosition.x, neighbourPosition.y, _gridSize.x);
                    if (closedList.Contains(neighbourNodeIndex)) {
                        // Already searched this node
                        continue;
                    }
                    PathNode neighbourNode = _pathNodeArray[neighbourNodeIndex];
                    if (!neighbourNode.isWalkable) {
                        // Not walkable
                        continue;
                    }
                    int2 currentNodePosition = new int2(currentNode.x, currentNode.y);
                    int tentativeGCost = currentNode.gCost + CalculateDistanceCost(currentNodePosition, neighbourPosition);
                    if (tentativeGCost < neighbourNode.gCost) {
                        neighbourNode.cameFromNodeIndex = currentNodeIndex;
                        neighbourNode.gCost = tentativeGCost;
                        neighbourNode.CalculateFCost();
                        _pathNodeArray[neighbourNodeIndex] = neighbourNode;
                        if (!openList.Contains(neighbourNode.index)) {
                            openList.Add(neighbourNode.index);
                        }
                    }
                }
            }
            PathNode endNode = _pathNodeArray[endNodeIndex];
            if (endNode.cameFromNodeIndex == -1) {
                // Didn't find a path!
                //Debug.Log("Didn't find a path!");
            } else {
                // Found a path
                NativeList<int2> path = CalculatePath(_pathNodeArray, endNode);
                /*
                foreach (int2 pathPosition in path) {
                    Debug.Log(pathPosition);
                }
                */
                path.Dispose();
            }
            //_pathNodeArray.Dispose();
            neighbourOffsetArray.Dispose();
            openList.Dispose();
            closedList.Dispose();
        }
        NativeList<int2> CalculatePath(NativeArray<PathNode> pathNodeArray, PathNode endNode) {
            if (endNode.cameFromNodeIndex == -1) {
                // Couldn't find a path!
                return new NativeList<int2>(Allocator.Temp);
            } else {
                // Found a path
                NativeList<int2> path = new NativeList<int2>(Allocator.Temp);
                path.Add(new int2(endNode.x, endNode.y));
                PathNode currentNode = endNode;
                while (currentNode.cameFromNodeIndex != -1) {
                    PathNode cameFromNode = pathNodeArray[currentNode.cameFromNodeIndex];
                    path.Add(new int2(cameFromNode.x, cameFromNode.y));
                    currentNode = cameFromNode;
                }
                return path;
            }
        }
        bool IsPositionInsideGrid(int2 gridPosition, int2 gridSize) {
            return
                gridPosition.x >= 0 &&
                gridPosition.y >= 0 &&
                gridPosition.x < gridSize.x &&
                gridPosition.y < gridSize.y;
        }
        int CalculateIndex(int x, int y, int gridWidth) {
            return x + y * gridWidth;
        }
        int CalculateDistanceCost(int2 aPosition, int2 bPosition) {
            int xDistance = math.abs(aPosition.x - bPosition.x);
            int yDistance = math.abs(aPosition.y - bPosition.y);
            int remaining = math.abs(xDistance - yDistance);
            return MOVE_DIAGONAL_COST * math.min(xDistance, yDistance) + MOVE_STRAIGHT_COST * remaining;
        }
        int GetLowestCostFNodeIndex(NativeList<int> openList, NativeArray<PathNode> pathNodeArray) {
            PathNode lowestCostPathNode = pathNodeArray[openList[0]];
            for (int i = 1; i < openList.Length; i++) {
                PathNode testPathNode = pathNodeArray[openList[i]];
                if (testPathNode.fCost < lowestCostPathNode.fCost) {
                    lowestCostPathNode = testPathNode;
                }
            }
            return lowestCostPathNode.index;
        }

    }
}

And the Grid, where dispose is called:

using UnityEngine;
using Unity.Mathematics;
using Unity.Collections;
using Unity.Jobs;
using Unity.Burst;

public class Grid : MonoBehaviour {
    public NativeArray<PathNode> pathNodeArray, grid;
    JobHandle jobHandle;
    GenerateGridJob gridJob;
    public int2 gridSize = new int2(10, 10);
    void Awake() {
        pathNodeArray = new NativeArray<PathNode>(gridSize.x * gridSize.y, Allocator.Persistent);
        grid = new NativeArray<PathNode>(gridSize.x * gridSize.y, Allocator.Persistent);
        GenerateGrid();
    }
    void Update() {
        if (jobHandle.IsCompleted) {
            jobHandle.Complete();
            grid = gridJob._pathNodeArray;
        }
    }
    void GenerateGrid() {
        jobHandle = new JobHandle();
        gridJob = new GenerateGridJob {
            _gridSize = gridSize,
            _pathNodeArray = pathNodeArray
        };
        jobHandle = gridJob.Schedule();
    }
    void OnDestroy() {
        pathNodeArray.Dispose();
        grid.Dispose();
    }

    [BurstCompile]
    struct GenerateGridJob : IJob {
        public int2 _gridSize;
        public NativeArray<PathNode> _pathNodeArray;

        public void Execute() {
            for (int x = 0; x < _gridSize.x; x++) {
                for (int y = 0; y < _gridSize.y; y++) {
                    PathNode pathNode = new PathNode();
                    pathNode.x = x;
                    pathNode.y = y;
                    pathNode.index = CalculateIndex(x, y, _gridSize.x);
                    //pathNode.gCost = int.MaxValue;
                    //pathNode.hCost = CalculateDistanceCost(new int2(x, y), _endPosition);
                    //pathNode.CalculateFCost();


                    //replace later
                    pathNode.isWalkable = true;



                    pathNode.cameFromNodeIndex = -1;
                    _pathNodeArray[pathNode.index] = pathNode;
                }
            }
            /*
            // Place Testing Walls
            {
                PathNode walkablePathNode = pathNodeArray[CalculateIndex(1, 0, gridSize.x)];
                walkablePathNode.SetIsWalkable(false);
                pathNodeArray[CalculateIndex(1, 0, gridSize.x)] = walkablePathNode;

                walkablePathNode = pathNodeArray[CalculateIndex(1, 1, gridSize.x)];
                walkablePathNode.SetIsWalkable(false);
                pathNodeArray[CalculateIndex(1, 1, gridSize.x)] = walkablePathNode;

                walkablePathNode = pathNodeArray[CalculateIndex(1, 2, gridSize.x)];
                walkablePathNode.SetIsWalkable(false);
                pathNodeArray[CalculateIndex(1, 2, gridSize.x)] = walkablePathNode;
            }
            */

            //_pathNodeArray.Dispose();
        }
        int CalculateIndex(int x, int y, int gridWidth) {
            return x + y * gridWidth;
        }
    }
}

And the errors:

ObjectDisposedException: The Unity.Collections.NativeArray`1[PathNode] has been deallocated, it is not allowed to access it
Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle.CheckDeallocateAndThrow (Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle handle) (at <40205bb2cb25478a9cb0f5e54cf11441>:0)
Unity.Collections.LowLevel.Unsafe.DisposeSentinel.Dispose (Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle& safety, Unity.Collections.LowLevel.Unsafe.DisposeSentinel& sentinel) (at <40205bb2cb25478a9cb0f5e54cf11441>:0)
Unity.Collections.NativeArray`1[T].Dispose () (at <40205bb2cb25478a9cb0f5e54cf11441>:0)
Grid.OnDestroy () (at Assets/Grid.cs:33)
A Native Collection has not been disposed, resulting in a memory leak. Allocated from:
Unity.Collections.NativeArray`1:.ctor(Int32, Allocator, NativeArrayOptions)
Grid:Awake() (at Assets\Grid.cs:14)

The problem is on line 20 of the grid script:

grid = gridJob._pathNodeArray;

This line is causing the variable grid to point to the same array as pathNodeArray. So when you try to dispose both arrays in OnDestroy, you get an error because they are actually the same array (so you’re trying to dispose an already disposed array), and you get the memory leak error when exciting play mode since the original array assigned to grid was never disposed.

1 Like

Inside the Grid MonoBehaviour it looks like you allocate 2 native arrays: pathNodeArray and grid. The GenerateGridJob struct also has a native array called _pathNodeArray. When creating the job you assign parhNodeArray to _pathNodeArray then when the job completes you assign _pathNodeArray to grid. So you no longer have the original Native Array that was allocated and assigned to grid. Now both grid and pathNodeArray are equal to pathNodeArray. So when you try to dispose both, you’re really trying to dispose the same one twice.

You probably don’t need 2 native arrays. Just have the one grid Native array. Create it in Awake, assign it to the job’s native array field, complete the job, then that’s it. Now the original grid NativeArray has the data you want. Code Monkey actually has a great video about how to get data out of a job using Native Arrays.

One other possible issue is that PathNode has a bool field and bools are managed objects, I believe. I remember having issues trying to put bools in Native Arrays. Seems like a silly issue though so hopefully Unity made an exception for bools and somehow made them allowed. If you end up having problems you could do something like this:

byte v;

public bool IsValueTrue

{

get => v > 0;

set => v = value ? 1 : 0;

}

But that’s horribly verbose compared to just using a bool

Good job! This looks really cool. I’ll have to check out his pathfinding tutorial at some point.

1 Like

Dang you beat me to it while I was writing mine!

I appreciate it. I got rid of this line and took out the grid variable. I’m running into more trouble, but that specific issue went away.

Hmmm … I wonder … because of the further issues: I think it’s possible for Destroy() to be called while a job is still running. In that case Destroy() should call Complete() on the JobHandle before disposing the arrays used by the job.

How do make it NOT do that?
Like, how do I copy it instead of pointing towards it?

I switched it to an int just to be safe.
It still gave me issues.

video link?

Code Monkey - How to get Output from the Unity Job System

If you post code/ error logs of the issues you’re having I’d be happy to take a look.

One thing to keep in mind:
NativeArrays (and all other Native Collections) are technically structs. So they are copied by-value, not by-reference. BUT… one of their fields is a pointer to memory, so they kind of are hybrid value-reference types.
Idk what the actual fields of NativeArray are, but pretend they’re this:

public struct NativeArray<T>
{
     // just for pretend
     T* pointer;
     int length;
}

All of the values that are “in” the native array are really in memory somewhere and that pointer points to them. So I can make a hundred copies of this native array and they will all point to the same collection of values. That was the problem you were having earlier. You overwrote one of your native arrays with another, so the original was still left in memory undisposed.