A Native Collection has not been disposed on AMD but not on intel

that’s in 2020.1.17 rocking burst 1.4.3

You should at least enable full stacktraces to get more details.
Jobs → Leak Detection → Full Stack Traces

probably this is not an “amd vs intel” but a thread timing issue. more+slower (amd) threads generally expose more race conditions than fewer+faster (intel)

2 Likes

interesting, how do I solve that? I’m not even using jobs here.

using UnityEngine;
using System.Collections.Generic;
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using Random = UnityEngine.Random;

[DefaultExecutionOrder(-9001)]
public class BlissManager : MonoBehaviour
{
    public static BlissManager _instance;
    public static BlissManager instance
    {
        get
        {
            if (!_instance)
            {
                _instance = FindObjectOfType(typeof(BlissManager)) as BlissManager;
                #if UNITY_EDITOR
                Debug.LogWarning("Expensive find object");
                #endif
            }
            return _instance;
        }
    }
    List<Booster> _boosters = new List<Booster>();
    Texture2D _tex = null;
    public float boostEffectMultiplier = 2f;
    List<Rect> _recalculate = new List<Rect>();
    const int HALF_GRID_SIZE = 100;
    public static int[] _grid;
    public int baseBliss = 0;
    float[] _lastMap = null;
    NativeArray<half> _newTextureMapHalfPointer;
    int _changeUMin = 0;
    int _changeUMax = 0;
    int _changeVMin = 0;
    int _changeVMax = 0;
    public RenderTexture blissRT;
    readonly half _good = (half) 1;
    readonly half _neutral = (half) 0.5;
    readonly half _bad = (half) 0;
    [Space] public bool testBlit = false;
    bool Paused => Time.timeScale < 0.1f;
    //job inout variables
    JobHandle recalculateBoostJobHandle;
    NativeArray<int> _gridNative;

    public void Awake()
    {
        if (_instance)
        {
            Destroy(gameObject);
            return;
        }
        _instance = this;
        _newTextureMapHalfPointer = new NativeArray<half>(HALF_GRID_SIZE * HALF_GRID_SIZE * 4, Allocator.Persistent);
        _lastMap = new float[HALF_GRID_SIZE * HALF_GRID_SIZE * 4];
        for (int i = 0; i < _lastMap.Length; i++) { _lastMap[i] = 0.5f; }
        _tex = new Texture2D(HALF_GRID_SIZE * 2, HALF_GRID_SIZE * 2, TextureFormat.RHalf, mipChain: true);
        //set the bliss map to 0.5 which is neutral
        var newMap = _tex.GetRawTextureData<half>();
        for (int i = 0; i < newMap.Length; i++) { newMap[i] = _neutral; }
        _tex.Apply(true);
        if (testBlit)
            Graphics.Blit(_tex, blissRT);
        else
            Graphics.CopyTexture(_tex, blissRT);
    }

    void OnEnable()
    {
        Shader.SetGlobalFloat("_BlissMapSize", HALF_GRID_SIZE * 2);
    }

    ParticleSystem _corruptionParticle;

    public void Start()
    {
        if (beautyCorrupted)
        {
            _corruptionParticle = (Instantiate(beautyCorrupted.gameObject, Vector3.zero, Quaternion.identity) as GameObject).GetComponent<ParticleSystem>();
            _corruptionParticle.enableEmission = false;
            _corruptionParticle.transform.parent = transform;
        }
        _changeVMax = HALF_GRID_SIZE * 2;
        _changeUMax = HALF_GRID_SIZE * 2;
        CalculateEntireBlissMap();
    }

    // emit particles
    void FixedUpdate()
    {
        if (performanceBeautifyEnabled)
        {
            for (int u = 0; u < HALF_GRID_SIZE * 2; u++)
            {
                for (int v = 0; v < HALF_GRID_SIZE * 2; v++)
                {
                    if (_beautyGrid[u, v] != null)
                    {
                        BeautyGrid beauty = _beautyGrid[u, v];
                        if (beauty.bliss <= .3f)
                        {
                            if (Random.value > CORRUPTIONPARTICLEPROBABILITY)
                            {
                                Vector3 randomCircle = Random.insideUnitSphere;
                                randomCircle.y = 0;
                                _corruptionParticle.Emit(beauty.position + randomCircle, Vector3.zero, 1, Random.Range(0.5f, 1.5f), Color.white);
                            }
                        }
                    }
                }
            }
        }
    }

    //recalculate bliss
    public void Update()
    {
        if (Paused)
            return;
        if (_recalculate.Count > 0)
        {
            Rect rmax = _recalculate[0];
            for (int i = 1; i < _recalculate.Count; i++)
            {
                Rect r = _recalculate[i];
                rmax.xMin = Mathf.Min(rmax.xMin, r.xMin);
                rmax.xMax = Mathf.Max(rmax.xMax, r.xMax);
                rmax.yMin = Mathf.Min(rmax.yMin, r.yMin);
                rmax.yMax = Mathf.Max(rmax.yMax, r.yMax);
            }
            RecalculateBoostJobified(rmax);
            _recalculate = new List<Rect>();
        }
    }

    public void RegisterBooster(Booster b)
    {
        _boosters.Add(b);
    }

    public void RemoveBooster(Booster b)
    {
        _boosters.Remove(b);
    }

    public float GetBoostMultiplier(Vector3 pos, bool negative)
    {
        int boost = GetBoost(pos);

        // are we boosted by corruption? if so -ve the boost
        if (negative) boost = -boost;

        // if boosted (i.e. positive bliss for good guys, negative for bad guys) return the multiplier
        if (boost <= 0) return 1;
        return boostEffectMultiplier;
    }

    public void QueueRecalculateBoost(Rect r)
    {
        _recalculate.Add(r);
    }

    public void CalculateEntireBlissMap()
    {
        RecalculateBoostJobified(new Rect(-HALF_GRID_SIZE, -HALF_GRID_SIZE, HALF_GRID_SIZE * 2, HALF_GRID_SIZE * 2));
    }

    struct Booster4Job
    {
        public Vector3 pos;
        public int range;
        public int boostAmount;
    }
    public struct NativeUnit<T> where T : struct
    {
        NativeArray<T> internalContainer;

        public NativeUnit(Allocator allocator)
        {
            internalContainer = new NativeArray<T>(1, allocator);
        }

        public void Dispose()
        {
            internalContainer.Dispose();
        }

        public T Value
        {
            get { return internalContainer[0]; }
            set { internalContainer[0] = value; }
        }
    }
    List<Booster4Job> _b4j = new List<Booster4Job>(1000);
    bool jobStarted = false;

    public void RecalculateBoostJobified(Rect r)
    {
        UnityEngine.Profiling.Profiler.BeginSample($"job grid bliss {r.width.ToString()}x{r.height.ToString()}");
        if (_grid == null)
            _grid = new int[HALF_GRID_SIZE * 2 * HALF_GRID_SIZE * 2];
        // prep native data for the job
        _newTextureMapHalfPointer = _tex.GetPixelData<half>(0);
        _b4j.Clear();
        foreach (Booster boost in _boosters)
        {
            if (!boost || !boost.gameObject.activeInHierarchy || !boost.boostEnabled)
                continue;
            _b4j.Add(new Booster4Job() {pos = boost.GetIntPosition(), range = boost.range, boostAmount = boost.boostAmount});
        }
        _gridNative = new NativeArray<int>(_grid, Allocator.TempJob);
        var recalculateBoostJob = new RecalculateBoostJob
        {
            r_IN = r,
            boosters4job_IN = new NativeArray<Booster4Job>(_b4j.ToArray(), Allocator.TempJob),
            baseBliss_IN = baseBliss,
            grid_OUT = _gridNative,
            newTextureMapHalfPointer_INOUT = _newTextureMapHalfPointer
        };
        recalculateBoostJob.Run();
        //unpack
        _gridNative.CopyTo(_grid);
        //clean up
        _gridNative.Dispose();
        // texture
        UnityEngine.Profiling.Profiler.BeginSample("apply texture");
        _tex.Apply(true);
        UnityEngine.Profiling.Profiler.EndSample();
        if (testBlit)
            Graphics.Blit(_tex, blissRT);
        else
            Graphics.CopyTexture(_tex, blissRT);
        UnityEngine.Profiling.Profiler.EndSample();
    }

    [BurstCompile]
    struct RecalculateBoostJob : IJob
    {
        [ReadOnly] public Rect r_IN;
        [ReadOnly] public NativeArray<Booster4Job> boosters4job_IN;
        [ReadOnly] public int baseBliss_IN;
        public NativeArray<int> grid_OUT;
        public NativeArray<half> newTextureMapHalfPointer_INOUT;

        public void Execute()
        {
            // init the blissmap
            var blissmap_OUT = new NativeArray<int>(HALF_GRID_SIZE * 2 * HALF_GRID_SIZE * 2, Allocator.Temp, NativeArrayOptions.ClearMemory);
            for (int i = 0; i < blissmap_OUT.Length; i++)
                blissmap_OUT[i] = 0;
            // convert to int and clamp
            int xMin = Mathf.Max((int) r_IN.xMin, -HALF_GRID_SIZE);
            int xMax = Mathf.Min((int) r_IN.xMax, HALF_GRID_SIZE);
            int zMin = Mathf.Max((int) r_IN.yMin, -HALF_GRID_SIZE);
            int zMax = Mathf.Min((int) r_IN.yMax, HALF_GRID_SIZE);
            // go through each booster
            int b;
            for (int i = 0; i < boosters4job_IN.Length; i++)
            {
                var boost = boosters4job_IN[i];
                var pos = boost.pos;
                int cx = (int) pos.x;
                int dx2 = cx + boost.range + 2;
                int dx1 = cx - boost.range - 1;
                int cz = (int) pos.z;
                int dz2 = cz + boost.range + 2;
                int dz1 = cz - boost.range - 1;
                // early exit if the booster isn't within the changed rect
                if (dx2 < xMin || dz2 < zMin || dx1 > xMax || dz1 > zMax)
                    continue;
                dx1 = Mathf.Max(dx1, xMin);
                dx2 = Mathf.Min(dx2, xMax);
                dz1 = Mathf.Max(dz1, zMin);
                dz2 = Mathf.Min(dz2, zMax);
                // convert current boost [-1,+1] to binary mask
                b = Boost2BlissBitmask(boost.boostAmount);
                int rangeSqr = boost.range * boost.range;
                for (int z = dz1; z < dz2; z++)
                {
                    for (int x = dx1; x < dx2; x++)
                    {
                        int distSqr = (x - cx) * (x - cx) + (z - cz) * (z - cz);
                        if (distSqr < rangeSqr)
                        {
                            var boostCoord = BoostCoord(x, z);
                            // bliss bitwise math
                            blissmap_OUT[boostCoord] = blissmap_OUT[boostCoord] | b;
                        }
                    }
                }
            }
            // composite pass with base bliss
            b = Boost2BlissBitmask(baseBliss_IN);
            for (int x = xMin; x < xMax; x++)
            {
                for (int z = zMin; z < zMax; z++)
                {
                    var boostCoord = BoostCoord(x, z);
                    grid_OUT[boostCoord] = blissmap_OUT[boostCoord] | b;
                }
            }
            // expand change rect for rendering
            int4 uv = new int4(Mathf.Min(HALF_GRID_SIZE * 2, HALF_GRID_SIZE + xMin), Mathf.Max(0, HALF_GRID_SIZE + xMax), Mathf.Min(HALF_GRID_SIZE * 2, HALF_GRID_SIZE + zMin), Mathf.Max(0, HALF_GRID_SIZE + zMax));
            // texture update
            for (int u = uv.x; u < uv.y; u++)
            {
                for (int v = uv.z; v < uv.w; v++)
                {
                    int coord = u + v * HALF_GRID_SIZE * 2;
                    int bliss = BlissBitmask2Boost(grid_OUT[coord]);
                    half newf = (half) (bliss == -1 ? 0 : bliss == 0 ? 0.5 : 1);
                    newTextureMapHalfPointer_INOUT[coord] = newf;
                }
            }
        }
    }

    static int Boost2BlissBitmask(int b)
    {
        if (b > 0)
            return 1;
        if (b < 0)
            return 2;
        return 0;
    }

    public static int BoostCoord(int x, int z)
    {
        if (x > HALF_GRID_SIZE - 1) x = HALF_GRID_SIZE - 1;
        if (z > HALF_GRID_SIZE - 1) z = HALF_GRID_SIZE - 1;
        if (x < -(HALF_GRID_SIZE - 1)) x = -(HALF_GRID_SIZE - 1);
        if (z < -(HALF_GRID_SIZE - 1)) z = -(HALF_GRID_SIZE - 1);
        return (x + HALF_GRID_SIZE) + (z + HALF_GRID_SIZE) * HALF_GRID_SIZE * 2;
    }

    public int GetBoost(Vector3 pos)
    {
        int x = Mathf.FloorToInt(pos.x + 0.001f);
        int z = Mathf.FloorToInt(pos.z + 0.001f);
        return GetBoost(x, z);
    }

    public static int GetBoost(int coord)
    {
        return BlissBitmask2Boost(_grid[coord]);
    }

    public int GetBoost(int x, int z)
    {
        if (_grid == null) return 0;
        return BlissBitmask2Boost(_grid[BoostCoord(x, z)]);
    }

    static int BlissBitmask2Boost(int b)
    {
        if (b == 1) return 1;
        if (b == 2) return -1;
        return 0;
    }

    int ConvertWorldToUVCoordinates(int2 worldCoordinates)
    {
        int result = 0;
        Physics.Raycast(new Vector3(worldCoordinates.x, 1, worldCoordinates.y), Vector3.down, out RaycastHit hit, 2);
        var uv = hit.textureCoord;
        return (int) uv.x + (int) uv.y * HALF_GRID_SIZE * 2;
    }

    public void OnDrawGizmosSelected()
    {
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireCube(Vector3.zero, new Vector3(HALF_GRID_SIZE * 2, 1, HALF_GRID_SIZE * 2));
    }

    // Beautification
    public bool performanceBeautifyEnabled;
    public GameObject beautifyBliss;
    public ParticleSystem beautyCorrupted;
    public LayerMask beautyLayer;
// what's on the terrain, optimization
    class BeautyGrid
    {
        public Vector3 position;
        public Vector3 normal;
        public float bliss;
        public GameObject blissObject;
        public ParticleSystem corruptionParticle;
    }
    BeautyGrid[,] _beautyGrid = new BeautyGrid[HALF_GRID_SIZE * 2, HALF_GRID_SIZE * 2];
    RaycastHit _hit = new RaycastHit();

    public void ForceRecalculateBoostAndUpdateFoliage()
    {
        RecalculateBoostJobified(new Rect(-HALF_GRID_SIZE, -HALF_GRID_SIZE, HALF_GRID_SIZE * 2, HALF_GRID_SIZE * 2));
    }

    void InstantiateBlissObject(int u, int v)
    {
        Vector3 n = _beautyGrid[u, v].normal;
        Quaternion rotation = Quaternion.AngleAxis(Random.value * 360, n);
        Vector3 randomOffset = Vector3.Cross(n, Random.insideUnitSphere);
        GameObject blissObjectInstance = Instantiate(beautifyBliss, _beautyGrid[u, v].position + randomOffset * .3f, rotation) as GameObject;
        //                    Color randomGrassColor = Random.Range(.5f,1)*Color.white;
        //                    randomGrassColor.a = 1;
        //                    blissObjectInstance.renderer.material.color = randomGrassColor;
        blissObjectInstance.transform.parent = transform;
        blissObjectInstance.SetActive(false);
        _beautyGrid[u, v].blissObject = blissObjectInstance;
    }

    void InstantiateCorruptionParticle(int u, int v)
    {
        _beautyGrid[u, v].corruptionParticle = (Instantiate(beautyCorrupted.gameObject, _beautyGrid[u, v].position, Quaternion.identity) as GameObject).GetComponent<ParticleSystem>();
        _beautyGrid[u, v].corruptionParticle.enableEmission = false;
        _beautyGrid[u, v].corruptionParticle.gameObject.SetActive(false);
        _beautyGrid[u, v].corruptionParticle.transform.parent = transform;
    }

//add or animate grass or lava to match bliss
    void Beautify(int u, int v, float bliss)
    {
        // turn off all particles and grass and skip if beautify is off
        if (!performanceBeautifyEnabled)
        {
            if (_beautyGrid[u, v] != null)
            {
                if (_beautyGrid[u, v].blissObject)
                    _beautyGrid[u, v].blissObject.SetActive(false);
                if (_beautyGrid[u, v].corruptionParticle)
                    _beautyGrid[u, v].corruptionParticle.enableEmission = false;
                _beautyGrid[u, v].bliss = 0;
            }
            return;
        }
        UnityEngine.Profiling.Profiler.BeginSample("beautify");
        //init beautify grid element
        if (_beautyGrid[u, v] == null)
        {
            if (Physics.Raycast(new Vector3(u - HALF_GRID_SIZE + .5f, 30, v - HALF_GRID_SIZE + .5f), -Vector3.up, out _hit, 40, beautyLayer))
            {
                _beautyGrid[u, v] = new BeautyGrid();
                _beautyGrid[u, v].position = _hit.point;
                _beautyGrid[u, v].normal = _hit.normal;
            }
        }
        if (_beautyGrid[u, v] == null)
            return;
        _beautyGrid[u, v].bliss = bliss;
        // calculate scale of bliss grass
        #if UNITY_IOS || UNITY_SWITCH
        // skip every 2 beautification
        if (u % 2 == 0 || v % 2 == 0)
            return;
        #endif
        if (bliss >= 0.7f)
        {
            //bliss
            if (!_beautyGrid[u, v].blissObject)
                InstantiateBlissObject(u, v);
            _beautyGrid[u, v].blissObject.SetActive(true);
            _beautyGrid[u, v].blissObject.transform.localScale = 2 * (bliss - .5f) * Vector3.one;
        } else
        {
            if (_beautyGrid[u, v].blissObject)
                _beautyGrid[u, v].blissObject.SetActive(false);
        }
        UnityEngine.Profiling.Profiler.EndSample();
    }
    #if UNITY_IOS || unity_
    const float CORRUPTIONPARTICLEPROBABILITY = 0.990f;
    #else
    const float CORRUPTIONPARTICLEPROBABILITY = 0.95f;
    #endif
}

Aren’t you just not disposing _newTextureMapHalfPointer that’s allocated in Awake?

That’s allocated as persistent, shouldn’t trigger these warnings. The only other allocation seems to be disposed correctly right after running the job which uses it on the main thread.

In my experience these warnings are unavoidable. Either Unity is misreporting or they have nothing to do with your code and are coming from Unity or one of its packages, because I see them in situations where everything has been checked, double checked and triple checked to be correct, and also in projects that don’t even use native collections themselves.

The uselessness of the warning just adds to the injury, because it tells you to enable a flag that can be only enabled by recompiling the Unity source code itself.

You messing warning about living more than expected allocations and what is OP talking about - exception about missed disposing of native collection, and Unity will and should throw error for that, in his case @tertle right, as this not disposed array - is obvious memory leak and on every domain reload Unity will throw exception as previous one not disposed.

Ah, yes. It’s indeed missing a dispose in OnDestroy(), since it was allocated in Awake().

That’s incorrect and how to turn that on has already been brought up in this thread:

So that doesn’t require a source code recompile. :wink:

Unrelated and super minor side note: (especially if you plan on using this in jobified and bursted code) you should consider using ProfilerMarkers and putting that dynamic info into the MetaData for the sample. The profiling overhead would then be lower and it’d be burst compatible. The meta info is then available in the timeline view selection Tooltip or the Related Data view in Hierarchy.

Aaaand I forgot the link to how to add that metadata to the samples.

1 Like

You’re right, I was thinking of the “JobTempAlloc has allocations that are more than 4 frames old” warning, which tells us to enable a define in some .cpp file.

Could be although this error happens when the thing starts to play, I added a dispose in OnDestroy as maybe something’s blowing it up the component at startup (games need a good spine) I’ll test on the AMD as the intel is working fine…

this option doesn’t exist in 1.4.3

nice thread hijacking, buddy :wink:
I’ll roll with it
what’s the syntax mean? the <> I get but not the extra strings in the (), is MySystem.Prepare some reflection stuff?

It looks like vanilla profiler with extra stuff outside the method to profile… how much faster is it?
6984032--824210--upload_2021-3-28_17-17-22.png
it’s not working with Rect? I don’t understand that error.

That’s still not the correct fix. You override it in RecalculateBoostJobified so your Awake allocation needs to be disposed before that.

I don’t know what to say about the Intel machine, if it’s not reporting this error on that then that’s a different issue. You have definitely implemented this wrong and it should be warning you about leaking.

you mean like that?
6984128--824258--upload_2021-3-28_18-24-47.png
if i do that i get this
6984128--824261--upload_2021-3-28_18-25-36.png

You need to dispose this array

_newTextureMapHalfPointer = new NativeArray<half>(HALF_GRID_SIZE * HALF_GRID_SIZE * 4, Allocator.Persistent);

But you can’t dispose this array.

_newTextureMapHalfPointer = _tex.GetPixelData<half>(0);

What you probably want to be doing is instead of assigning GetPixelData to _newTextureMapHalfPointer, you should probably be doing something like

_newTextureMapHalfPointer.CopyFrom(_tex.GetPixelData<half>(0));

and then just disposing _newTextureMapHalfPointer in OnDestroy

(I haven’t read your code deep to actually check this)

Is this different from Profiler.BeginSample()/EndSample()? Or does BeginSample() or EndSample() use ProfilerMarker internally?

Not sure about the implementation but they work the same.

ProfilerMarker can be created direct in a burst job as of burst 1.5 or can be passed to a burst job in 1.4

Ehem… :sweat_smile: I was about to comment on the actual thread but then most of that was already covered by the time I got to that …

And no it’s no reflection. The first string is just the marker name, the following ones are the names for the meta data points, defined as basic value types via the generic type parameters. So that would be:

static readonly ProfilerMarker s_GridBlissMarker = new ProfilerMarker<float,float>("job grid bliss", "width", "height");
// then
s_GridBlissMarker.Begin(r.width, r.height);
// code
s_GridBlissMarker.End();
// or
using (s_GridBlissMarker.Auto(r.width, r.height)){
    // code
}

And you need to install com.unity.profiling.core, which is getting ready to be verified/released as 1.0 and compatible with 2020.1+

I don’t have a hard number for you but lets compare what they each need to do:
UnityEngine.Profiling.Profiler.BeginSample($“job grid bliss {r.width.ToString()}x{r.height.ToString()}”)

  • 2 ToString allocs, one string format alloc

  • null or empty check on the marker name

  • Marshall / cast that string memory to prepare it for use in native code

  • It always assumes you used a context object (second param) so it’ll do a null check on that one

  • Then emit the full name as native char array into the profiler stream as a marker metadata, writing in 4 byte strides, alongside the int 0 for the context object and an int for the category (Scripts).

s_GridBlissMarker.Begin(r.width, r.height);

  • Pass an IntPtr to native

  • Cast that from void* to ProfilerMarker*
    (then nullcheck and if null fall back to using a “” marker)

  • Cast the metadata array from void* to a metadata struct (it’s all blitable data so…)

  • An int comparison to check the marker has a valid ID

  • Emit these as 3 32bit values (pretty much type agnostic at this point) to the Profiler Stream

There’s a bit extra going on but that’s the most essential differences in terms of computation. Also, since ProfilerBegin doesn’t actually create a marker but just uses a default one with string metadata, you can’t record these samples with Recoder / ProfilerRecorder APIs and therefore e.g. not use them in perfomance tests or other in-player measurements. And yes, there’s the Burstability thing due to ProfilerMarker being only blittable types.

The meta data names and marker names are only written to the stream once on creation and from then on only referred to via the marker ID.

Plus the Auto() scoped helper avoids mismatching Begin & End calls due to exceptions or early return calls in the measured scope, even if that comes at the “cost” of one IDisposable struct creation, that’s still faster than the old Profiler.Begin / End thing. Also, Auto can’t be fully compiled out for Release builds but it’ll just return a hollowed out disposable struct.

I mean, unless you emit a lot of these it’s a bit of a micro optimization but you want to measure your code, and not how fast Unity can marshal and write a string so… Also the biggest perf advantage here is likely the string creation, which in the worst-case-scenario actually triggers GC.Collect and then it’s “good bye” to sensible measurements :wink:

2 Likes