EntityCommandBuffer disposed in Unity.Entities.EndSimulationEntityCommandBufferSystem.

Hi everyone !

I’ve been stuck with the EntityCommandBuffer for 3 weeks now. I’ve seen this post with a similar issue but the fix was already done on my side. The bug happens not always, making it difficult to resolve.

Here is my code :

using System.Collections.Generic;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
[UpdateAfter(typeof(SpawnSystem))]
public class CollectDataSystem : SystemBase
{
    private EntityQuery m_botPlayerQuery;
    private List<SharedBotPlayerData> m_botPlayerFilters;
    private EndSimulationEntityCommandBufferSystem m_endSimulationEcbSystem;
    protected override void OnCreate()
    {
        m_botPlayerQuery = GetEntityQuery(ComponentType.ReadOnly<SharedBotPlayerData>());
        m_endSimulationEcbSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    }
    protected override void OnStartRunning()
    {
        m_botPlayerFilters = new List<SharedBotPlayerData>(Utils.Parameters.MaxPlayerNumber + 1);
    }
    protected override void OnUpdate()
    {
        int botNumber = World.GetExistingSystem<SpawnSystem>().m_botNumber;
        //UnityEngine.Debug.Log($"Number of bots: {botNumber}");
        EntityManager.GetAllUniqueSharedComponentData(m_botPlayerFilters);
        NativeArray<int> playerIdArray = new NativeArray<int>(Utils.Parameters.MaxPlayerNumber + 1, Allocator.TempJob);
        NativeArray<int> botPlayerNumberArray = new NativeArray<int>(Utils.Parameters.MaxPlayerNumber + 1, Allocator.TempJob);
        NativeArray<int> botPlayerOffsetArray = new NativeArray<int>(Utils.Parameters.MaxPlayerNumber + 1, Allocator.TempJob);
        NativeHashMap<int, bool> isLeaderEatenMap = new NativeHashMap<int, bool>(Utils.Parameters.MaxPlayerNumber, Allocator.TempJob);
        NativeArray<Entity> entityArray = new NativeArray<Entity>(botNumber, Allocator.TempJob);
        NativeArray<bool> isLeaderArray = new NativeArray<bool>(botNumber, Allocator.TempJob);
        NativeArray<int> botPlayerIdArray = new NativeArray<int>(botNumber, Allocator.TempJob);
        NativeArray<float3> positionArray = new NativeArray<float3>(botNumber, Allocator.TempJob);
        NativeArray<quaternion> rotationArray = new NativeArray<quaternion>(botNumber, Allocator.TempJob);
        NativeHashMap<int, int> eatenBotToPlayerMap = new NativeHashMap<int, int>(botNumber, Allocator.TempJob);
        int indexOffset = 0;
        #region Initialize data
        for (int playerTypeIndex = 0; playerTypeIndex < m_botPlayerFilters.Count; ++playerTypeIndex)
        {
            SharedBotPlayerData botPlayerFilter = m_botPlayerFilters[playerTypeIndex];
            playerIdArray[playerTypeIndex] = botPlayerFilter.m_playerId;
            // Filter bots by playerId
            m_botPlayerQuery.AddSharedComponentFilter(botPlayerFilter);
            int botPlayerCount = m_botPlayerQuery.CalculateEntityCount();
            botPlayerNumberArray[playerTypeIndex] = botPlayerCount;
            botPlayerOffsetArray[playerTypeIndex] = indexOffset;
        
            // Player doesn't have any follower
            if (botPlayerCount == 0)
            {
                m_botPlayerQuery.ResetFilter();
                continue;
            }
            JobHandle entityJobHandle = Entities
                .WithName("entityJob")
                .WithSharedComponentFilter(botPlayerFilter)
                .WithNativeDisableParallelForRestriction(entityArray)
                .ForEach((int entityInQueryIndex, in Entity _entity) =>
                {
                    entityArray[indexOffset + entityInQueryIndex] = _entity;
                })
                .ScheduleParallel(Dependency);
            JobHandle positionJobHandle = Entities
                .WithName("positionJob")
                .WithSharedComponentFilter(botPlayerFilter)
                .WithNativeDisableParallelForRestriction(positionArray)
                .ForEach((int entityInQueryIndex, in Translation _translation) =>
                {
                    positionArray[indexOffset + entityInQueryIndex] = _translation.Value;
                })
                .ScheduleParallel(Dependency);
            JobHandle rotationJobHandle = Entities
                .WithName("rotationJob")
                .WithSharedComponentFilter(botPlayerFilter)
                .WithNativeDisableParallelForRestriction(rotationArray)
                .ForEach((int entityInQueryIndex, in Rotation _rotation) =>
                {
                    rotationArray[indexOffset + entityInQueryIndex] = _rotation.Value;
                })
                .ScheduleParallel(Dependency);
            JobHandle botPlayerJobHandle = Entities
                .WithName("botPlayerJob")
                .WithSharedComponentFilter(botPlayerFilter)
                .WithNativeDisableParallelForRestriction(botPlayerIdArray)
                .ForEach((int entityInQueryIndex, in BotPlayerData _botPlayerData) =>
                {
                    botPlayerIdArray[indexOffset + entityInQueryIndex] = _botPlayerData.m_playerId;
                })
                .ScheduleParallel(Dependency);
            JobHandle isLeaderJobHandle = Entities
                .WithName("isLeaderJob")
                .WithSharedComponentFilter(botPlayerFilter)
                .WithNativeDisableParallelForRestriction(isLeaderArray)
                .ForEach((int entityInQueryIndex, in BotTypeData _botTypeData) =>
                {
                    isLeaderArray[indexOffset + entityInQueryIndex] = _botTypeData.m_botType == BotType.Leader;
                })
                .ScheduleParallel(Dependency);
            JobHandle collectDataJobHandle = Dependency;
            collectDataJobHandle = JobHandle.CombineDependencies(collectDataJobHandle, entityJobHandle);
            collectDataJobHandle = JobHandle.CombineDependencies(collectDataJobHandle, positionJobHandle);
            collectDataJobHandle = JobHandle.CombineDependencies(collectDataJobHandle, rotationJobHandle);
            collectDataJobHandle = JobHandle.CombineDependencies(collectDataJobHandle, botPlayerJobHandle);
            collectDataJobHandle = JobHandle.CombineDependencies(collectDataJobHandle, isLeaderJobHandle);
            Dependency = collectDataJobHandle;
            // Reset
            m_botPlayerQuery.AddDependency(Dependency);
            m_botPlayerQuery.ResetFilter();
            indexOffset += botPlayerCount;
        }
        m_botPlayerFilters.Clear();
        #endregion
        #region Process data     
        EntityCommandBuffer entityCommandBuffer = m_endSimulationEcbSystem.CreateCommandBuffer();
    
        // Eat
        EatJob eatJob = new EatJob
        {
            m_botNumber = botNumber,
            m_botPlayerNumberArray = botPlayerNumberArray,
            m_botPlayerIdArray = botPlayerIdArray,
            m_positionArray = positionArray,
            m_isLeaderArray = isLeaderArray,
            m_eatenBotToPlayerMap = eatenBotToPlayerMap
        };
        JobHandle eatJobHandle = eatJob.Schedule(botNumber, 1, Dependency);
    
        UpdateBotsJob updateBotsJob = new UpdateBotsJob
        {
            m_botNumber = botNumber,
            m_entityArray = entityArray,
            m_botPlayerNumberArray = botPlayerNumberArray,
            m_botPlayerOffsetArray = botPlayerOffsetArray,
            m_botPlayerIdArray = botPlayerIdArray,
            m_isLeaderArray = isLeaderArray,
            m_eatenBotToPlayerMap = eatenBotToPlayerMap,
            m_commandBuffer = entityCommandBuffer
        };
        JobHandle updateBotsJobHandler = updateBotsJob.Schedule(botNumber, 64, eatJobHandle);
        m_endSimulationEcbSystem.AddJobHandleForProducer(updateBotsJobHandler);
        Dependency = updateBotsJobHandler;
        #endregion
        #region Dispose data
        JobHandle disposeJobHandle = playerIdArray.Dispose(Dependency);
        disposeJobHandle = JobHandle.CombineDependencies(disposeJobHandle, botPlayerNumberArray.Dispose(Dependency));
        disposeJobHandle = JobHandle.CombineDependencies(disposeJobHandle, botPlayerOffsetArray.Dispose(Dependency));
        disposeJobHandle = JobHandle.CombineDependencies(disposeJobHandle, isLeaderEatenMap.Dispose(Dependency));
        disposeJobHandle = JobHandle.CombineDependencies(disposeJobHandle, entityArray.Dispose(Dependency));
        disposeJobHandle = JobHandle.CombineDependencies(disposeJobHandle, isLeaderArray.Dispose(Dependency));
        disposeJobHandle = JobHandle.CombineDependencies(disposeJobHandle, botPlayerIdArray.Dispose(Dependency));
        disposeJobHandle = JobHandle.CombineDependencies(disposeJobHandle, positionArray.Dispose(Dependency));
        disposeJobHandle = JobHandle.CombineDependencies(disposeJobHandle, rotationArray.Dispose(Dependency));
        disposeJobHandle = JobHandle.CombineDependencies(disposeJobHandle, eatenBotToPlayerMap.Dispose(Dependency));
        Dependency = disposeJobHandle;
        #endregion
    }
    struct EatJob : IJobParallelFor
    {
        [ReadOnly]
        public int m_botNumber;
        [ReadOnly]
        public NativeArray<int> m_botPlayerNumberArray;
        [ReadOnly]
        public NativeArray<int> m_botPlayerIdArray;
        [ReadOnly]
        public NativeArray<float3> m_positionArray;
        [ReadOnly]
        public NativeArray<bool> m_isLeaderArray;
        [NativeDisableParallelForRestriction]
        public NativeHashMap<int, int> m_eatenBotToPlayerMap;
        // The code actually running on the job
        public void Execute(int index)
        {
            // Get current playerId
            int botPlayerId = m_botPlayerIdArray[index];
            int botPlayerNumber = m_botPlayerNumberArray[botPlayerId];
            // If the bot is a civilian
            if (botPlayerId == 0)
            {
                return;
            }
        
            // Iterate on each entity
            for (int botIndex = 0; botIndex < m_botNumber; ++botIndex)
            {
                int otherBotPlayerId = m_botPlayerIdArray[botIndex];
                // If they are in the same team
                if (otherBotPlayerId == botPlayerId)
                {
                    continue;
                }
                // If the entity is a civilian
                if (otherBotPlayerId == SpawnSystem.CIVILIAN_ID)
                {
                    // If the entity is close to the current position
                    if (AreClose(index, botIndex))
                    {
                        ChangeToPlayer(botIndex, botPlayerId);
                    }
                }
                // Else the entity belong to another team
                else
                {
                    int otherBotPlayerNumber = m_botPlayerNumberArray[otherBotPlayerId];
                    // If the other team is equal or bigger than the current one, skip it
                    if (botPlayerNumber <= otherBotPlayerNumber)
                    {
                        continue;
                    }
                    // If the entity is close to the current position
                    if (AreClose(index, botIndex))
                    {
                        ChangeToPlayer(botIndex, botPlayerId);
                    }
                }
            }
        }
        private bool AreClose(int _left, int _right) => math.distance(m_positionArray[_left], m_positionArray[_right]) < 1.5f;
        private void ChangeToPlayer(int _botIndex, int _botPlayerId)
        {
            m_eatenBotToPlayerMap[_botIndex] = _botPlayerId;
        }
    }
    struct UpdateBotsJob : IJobParallelFor
    {
        [ReadOnly]
        public int m_botNumber;
        [ReadOnly]
        public NativeArray<Entity> m_entityArray;
        [ReadOnly]
        public NativeArray<int> m_botPlayerNumberArray;
        [ReadOnly]
        public NativeArray<int> m_botPlayerOffsetArray;
        [ReadOnly]
        public NativeArray<int> m_botPlayerIdArray;
        [ReadOnly]
        public NativeArray<bool> m_isLeaderArray;
        [ReadOnly]
        public NativeHashMap<int, int> m_eatenBotToPlayerMap;
        [NativeDisableParallelForRestriction]
        public EntityCommandBuffer m_commandBuffer;
        // The code actually running on the job
        public void Execute(int index)
        {
            if (!m_eatenBotToPlayerMap.ContainsKey(index))
            {
                return;
            }
            int eatenPlayerId = m_eatenBotToPlayerMap[index];
            // Change team
            ChangeToPlayer(index, eatenPlayerId);
            // If it was a leader
            if (m_isLeaderArray[index])
            {
                SetNewLeader(index);
            }
        }
        private void ChangeToPlayer(int _botIndex, int _playerId)
        {
            m_commandBuffer.SetComponent(m_entityArray[_botIndex], new BotPlayerData { m_playerId = _playerId });
            m_commandBuffer.SetSharedComponent(m_entityArray[_botIndex], new SharedBotPlayerData { m_playerId = _playerId });
            m_commandBuffer.SetSharedComponent(m_entityArray[_botIndex], new SharedBotTypeData { m_botType = BotType.Follower });
        }
        private void SetNewLeader(int _botIndex)
        {
            int botPlayerId = m_botPlayerIdArray[_botIndex];
            int playerOffset = m_botPlayerOffsetArray[botPlayerId];
            int playerCount = m_botPlayerNumberArray[botPlayerId];
            for (int botAllyIndex = playerOffset; botAllyIndex < playerOffset + playerCount; ++botAllyIndex)
            {
                int allyPlayerId = m_botPlayerIdArray[botAllyIndex];
                // If the ally has been eaten this frame
                if (m_eatenBotToPlayerMap.ContainsKey(botAllyIndex))
                {
                    continue;
                }
                // TODO: Filter on closest ally
                m_commandBuffer.SetSharedComponent(m_entityArray[botAllyIndex], new SharedBotTypeData { m_botType = BotType.Leader });
                break;
            }
        }
    }
}

Some explanations :

  • There are teams, consisted of “bots”, moving with a ‘boids’ algo, by following a leader.
  • I collect data from bots
  • If a bot is near a bot from another team, I check who has the larger team. If the first team is bigger, the bot eats the other one; this is what EatJob does.
  • Thanks to the UpdateBotsJob, I fill up the command buffer to change data
  • I notify to do changes in the EndSimulationSystem.
  • I dispose data

However, I got this error:

ArgumentException: Handle is not initialized.
EntityCommandBuffer was recorded in CollectDataSystem and disposed in Unity.Entities.EndSimulationEntityCommandBufferSystem.
  at System.Runtime.InteropServices.GCHandle.Free () [0x00021] in <9577ac7a62ef43179789031239ba8798>:0
  at Unity.Entities.EntityCommandBuffer.FreeChain (Unity.Entities.EntityCommandBufferChain* chain, Unity.Entities.PlaybackPolicy playbackPolicy, System.Boolean didPlayback) [0x00014] in {PROJECT}\Library\PackageCache\com.unity.entities@0.17.0-preview.41\Unity.Entities\EntityCommandBuffer.cs:1230
  at Unity.Entities.EntityCommandBuffer.Dispose () [0x00034] in {PROJECT}\Library\PackageCache\com.unity.entities@0.17.0-preview.41\Unity.Entities\EntityCommandBuffer.cs:1203
  at Unity.Entities.EntityCommandBufferSystem.FlushPendingBuffers (System.Boolean playBack) [0x0010c] in {PROJECT}\Library\PackageCache\com.unity.entities@0.17.0-preview.41\Unity.Entities\EntityCommandBufferSystem.cs:257
Unity.Entities.EntityCommandBufferSystem.FlushPendingBuffers (System.Boolean playBack) (at Library/PackageCache/com.unity.entities@0.17.0-preview.41/Unity.Entities/EntityCommandBufferSystem.cs:294)
Unity.Entities.EntityCommandBufferSystem.OnUpdate () (at Library/PackageCache/com.unity.entities@0.17.0-preview.41/Unity.Entities/EntityCommandBufferSystem.cs:192)
Unity.Entities.ComponentSystem.Update () (at Library/PackageCache/com.unity.entities@0.17.0-preview.41/Unity.Entities/ComponentSystem.cs:114)
Unity.Entities.ComponentSystemGroup.UpdateAllSystems () (at Library/PackageCache/com.unity.entities@0.17.0-preview.41/Unity.Entities/ComponentSystemGroup.cs:472)
UnityEngine.Debug:LogException(Exception)
Unity.Debug:LogException(Exception) (at Library/PackageCache/com.unity.entities@0.17.0-preview.41/Unity.Entities/Stubs/Unity/Debug.cs:19)
Unity.Entities.ComponentSystemGroup:UpdateAllSystems() (at Library/PackageCache/com.unity.entities@0.17.0-preview.41/Unity.Entities/ComponentSystemGroup.cs:477)
Unity.Entities.ComponentSystemGroup:OnUpdate() (at Library/PackageCache/com.unity.entities@0.17.0-preview.41/Unity.Entities/ComponentSystemGroup.cs:417)
Unity.Entities.ComponentSystem:Update() (at Library/PackageCache/com.unity.entities@0.17.0-preview.41/Unity.Entities/ComponentSystem.cs:114)
Unity.Entities.DummyDelegateWrapper:TriggerUpdate() (at Library/PackageCache/com.unity.entities@0.17.0-preview.41/Unity.Entities/ScriptBehaviourUpdateOrder.cs:333)

I think I missed something, perhaps something special with SharedComponent… Any ideas/suggestions?

Thanks :wink:

Never use NativeDisableParallelForRestriction on an EntityCommandBuffer. That is not even close to safe in any imaginable circumstance.

2 Likes

In addition to what @DreamingImLatios told above - you should use EntityCommandBuffer.ParallelWriter version.
on 116 row add var entityCommandBufferParallel = entityCommandBuffer.AsParallelWriter() and use it for parallel jobs.

2 Likes

Thank you both !

@DreamingImLatios Indeed, the attribute ‘NativeDisableParallelForRestriction’ was the issue.
May I ask you why it is not recommanded ? I would say because there is an EntityCommandBuffer.ParallelWriter as mentionned by @eizenhorn ?

I works like a charm now \o/

1 Like

Yes. The non-parallel version writes commands to a single threaded stream using a single counter, whereas the ParallelWriter has a stream and counter per thread.

1 Like