Issue allocating native array and storing the pointer but disposing of the struct

Consider this code:
code

public unsafe struct AdjacencyShortMap : INativeDisposable, IDisposable {
    private bool Valid;
    private const int MaxResize = 8;
    public TileGrid TileGrid;
    public int Floor;
    public int XOffset;
    public int ZOffset;
    public int Width;
    public int Depth;
    public short* Data;

    public AdjacencyShortMap(TileGrid TileGrid, int Floor) {
        this.TileGrid = TileGrid;
        this.Floor = Floor;
        XOffset = (int)TileGrid.Position.x;
        ZOffset = (int)TileGrid.Position.y;
        Width = MaxResize + 2;
        Depth = MaxResize + 2;
        Data = (short*)new NativeArray<short>(Width * Depth * 4, Allocator.Persistent, NativeArrayOptions.ClearMemory).GetUnsafePtr();
        Valid = true;
    }

    public void DrawDebug(float y) {
        Vector3 ZeroGrid = new Vector3(TileGrid.GridOffset.x, 0, TileGrid.GridOffset.y);
        Vector3 xline = (Quaternion)TileGrid.Rotation * Vector3.back;
        Vector3 zline = (Quaternion)TileGrid.Rotation * Vector3.left;
        for (int x = 0; x < MaxResize * 2; x++) {
            for (int z = 0; z < MaxResize * 2; z++) {
                Vector3 start = (TileGrid.Position.x + x / 2f) * xline + (TileGrid.Position.y + z / 2f) * zline + new Vector3(0, y, 0) + ZeroGrid;
                short data = Data[x + z * Width * 2];
                for (int i = 0; i < 16; i++) {
                    Vector3 up = Vector3.up / 10;
                    Debug.DrawLine(start + up * i, start + up * (i + 1), ((data & (1 << i)) != 0) ? Color.red : Color.green);
                }
            }
        }
    }

    public JobHandle Dispose(JobHandle inputDeps) {
        UnsafeUtility.Free(Data, Allocator.Persistent);
        return default;
    }

    public void Dispose() {
        UnsafeUtility.Free(Data, Allocator.Persistent);
    }

I am fairly sure the Data array is properly disposed when I am done with it, yet as soon as I create the AdjacencyShortMap it complains that the native array was not properly disposed. I assume this is happening because I let go of the newly created native array reference and it gets GC’ed or something. Even though I store the pointer and (probably) properly dispose of it later on.
How can I do this exact same thing without getting the A Native Collection has not been disposed, resulting in a memory leak. errors? My objective is to be able to use native arrays of AdjacencyShortMap in my jobs, so I can’t actually have NativeArray<short>s on it

The quick answer is that you should not be creating a new NativeArray for this as that also generates an AtomicSafetyHandle and DisposeSentinel.

The better answer is that your array of arrays should really be a custom NativeContainer that handles the AtomicSafetyHandle and DisposeSentinel, especially if this is something you plan to use in a bunch of different locations.

1 Like

@Guedez was there a reason not to use UnsafeUtility.Malloc in this case?

1 Like

I was using NativeArray specifically to avoid having to figure out how to use those.

Because I don’t know how to use it.
What’s the Alignment for and what’s Size measured in? If I want 256 shorts allocated, what does the method call looks like?

Eventually there comes a point in your DOTS journey where you must learn to harness their power. I suspect that time may have come for you.

Bytes. The size is how many bytes of memory you want, and the alignment specifies that the first address be a multiple of that value, which is required for certain data types to be read correctly. You can get the alignment using UnsafeUtiltity.AlignOf().

This is untested but it might be helpful. It’s a typed malloc for unmanaged types that does some similar sanity checks to NativeArray:

public static unsafe T* MallocUnmanaged<T>(int length, Allocator allocator) where T : unmanaged
{
    var totalSize = UnsafeUtility.SizeOf<T>() * (long) length;
    if (allocator <= Allocator.None)
    {
        throw new System.ArgumentException("Allocator must be Temp, TempJob or Persistent", nameof (allocator));
    }
    else if (length < 0)
    {
        throw new System.ArgumentOutOfRangeException(nameof (length), "Length must be >= 0");
    }
    else if (totalSize > int.MaxValue)
    {
        throw new System.ArgumentOutOfRangeException(nameof (length),
            $"Length * sizeof(T) cannot exceed {int.MaxValue} bytes");
    }
    return (T*)UnsafeUtility.Malloc(totalSize, UnsafeUtility.AlignOf<T>(), allocator);
}

You’d use it like this: var ptr = MallocUnmanaged<ushort>(20, Allocator.Persistent);

p.s. sorry for not testing it, I’m a little lost in other code right now. Do let me know if it doesn’t work and I’ll fix it up.
p.p.s. I made this specifically for unmanaged types as that way we can return a typed pointer. If you want it for more structs you can change T* to void* and : unmanaged to : struct

3 Likes

It (probably) worked just fine. I’ve even (probably) added the clear memory part.

    public static unsafe T* MallocUnmanaged<T>(int length, Allocator allocator, NativeArrayOptions Extra = NativeArrayOptions.UninitializedMemory) where T : unmanaged {
        var totalSize = UnsafeUtility.SizeOf<T>() * (long)length;
        if (allocator <= Allocator.None) {
            throw new System.ArgumentException("Allocator must be Temp, TempJob or Persistent", nameof(allocator));
        } else if (length < 0) {
            throw new System.ArgumentOutOfRangeException(nameof(length), "Length must be >= 0");
        } else if (totalSize > int.MaxValue) {
            throw new System.ArgumentOutOfRangeException(nameof(length),
                $"Length * sizeof(T) cannot exceed {int.MaxValue} bytes");
        }
        T* ptr = (T*)UnsafeUtility.Malloc(totalSize, UnsafeUtility.AlignOf<T>(), allocator);
        if (Extra == NativeArrayOptions.ClearMemory) {
            UnsafeUtility.MemClear(ptr, totalSize);
        }
        return ptr;
    }

At the very least, there were no crashes, out of bounds memory access and other kind of errors I would expect if this was not working.

1 Like

Ah, so this is what they meant by performance by default?

No.

There’s nothing default about a custom data structure for a custom problem.

1 Like

We got 99 problems but performance shouldn’t be one of them.

1 Like