EntityCommandBuffer is creating entities with negative indices and zero versions

I have a static factory method that creates an entity with some components. It’s overloaded: one method takes in EntityCommandBuffer while the other takes in a EntityCommandBuffer.Concurrent and an index. The concurrent one works fine and produces a valid entity with the components. However, the sequential one is producing an entity with a negative index and a zeroed version! Here are the factory methods

public static class MessageFactory
{
    public static Entity CreateDelayedMessage(
        EntityCommandBuffer ecb,
        Entity sender,
        Entity receiver,
        float secondsToWait
    )
    {
        var newMessage = ecb.CreateEntity();
        ecb.AddComponent(newMessage, new MessageReceiver
        {
            receiver = receiver
        });
        ecb.AddComponent(newMessage, new MessageSender
        {
            sender = sender
        });
        ecb.AddComponent(newMessage, new WaitStartMessage
        {
            secondsToWait = secondsToWait
        });
        return newMessage;
    }
    public static Entity CreateDelayedMessage(
        EntityCommandBuffer.Concurrent ecb,
        int jobIndex,
        Entity sender,
        Entity receiver,
        float secondsToWait
    )
    {
        var newMessage = ecb.CreateEntity(jobIndex);
        ecb.AddComponent(jobIndex,newMessage, new MessageReceiver
        {
            receiver = receiver
        });
        ecb.AddComponent(jobIndex,newMessage, new MessageSender
        {
            sender = sender
        });
        ecb.AddComponent(jobIndex,newMessage, new WaitStartMessage
        {
            secondsToWait = secondsToWait
        });
        return newMessage;
    }
}

And here is the job, which is scheduled sequentially (ScheduleSingle)

    private struct ReceiveCommunicationJob : IJobForEachWithEntity<AttackingCommunicationWaiter, WaitCompleteMessage,MessageReceiver, MessageSender>
    {
        public EntityCommandBuffer ecb;
        public BufferFromEntity<AttackingCommunicatedAlliesElement> communicatedAlliesFromEntity;
        public ComponentDataFromEntity<BlackboardType> blackboardFromEntity;
        public ComponentDataFromEntity<GlobalAttackingBlackboard> globalBlackboardFromEntity;
        public ComponentDataFromEntity<AttackingCommunication> communicationFromEntity;
        public BufferFromEntity<AttackingCommunicationTrackedWaiterElement> trackedWaitersFromEntity;
        public BufferFromEntity<AllyWithinSpeakingRangeElement> speakingRangeAlliesFromEntity;

        public void Execute(
            Entity waiterEntity,
            int index,
            [ReadOnly] ref AttackingCommunicationWaiter waiter,
            [ReadOnly] ref WaitCompleteMessage finishedMessage,
            [ReadOnly] ref MessageReceiver receiver,
            [ReadOnly] ref MessageSender sender
        )
        {
            var recipientEntity = receiver.receiver;

            var recipientBlackboard = blackboardFromEntity[recipientEntity];
            var recipientBlackboardData = recipientBlackboard.GetBlackboard();
            var senderEntity = sender.sender;
            var senderBlackboard = blackboardFromEntity[senderEntity];
            var senderBlackboardData = senderBlackboard.GetBlackboard();
            if (senderBlackboardData.FresherThen(recipientBlackboardData))
            {
                recipientBlackboardData.ReplaceWith(senderBlackboardData);
            }
            recipientBlackboard.SetBlackboard(recipientBlackboardData);
            blackboardFromEntity[recipientEntity] = recipientBlackboard;

            var recipientGlobalBlackboardData = globalBlackboardFromEntity[recipientEntity];
            var senderGlobalBlackboardData = globalBlackboardFromEntity[senderEntity];
            if (senderGlobalBlackboardData.FresherThen(recipientGlobalBlackboardData))
            {
                recipientGlobalBlackboardData.ReplaceWith(recipientGlobalBlackboardData);
            }
            globalBlackboardFromEntity[recipientEntity] = recipientGlobalBlackboardData;

            var trackerEntity = waiter.tracker;
            var trackedWaiters = trackedWaitersFromEntity[trackerEntity];
            Debug.Log("Before: " + trackedWaiters.Length);
            for(int i = 0; i < trackedWaiters.Length; i++)
            {
                Debug.Log("Current waiters: " + trackedWaiters[i].waiter.ToString());
            }
            trackedWaiters.Remove(new AttackingCommunicationTrackedWaiterElement
            {
                waiter = waiterEntity
            });
            Debug.Log(trackedWaiters.Length + " due to removing: " + waiterEntity.ToString());

            var communicatedAllies = communicatedAlliesFromEntity[trackerEntity];

            var recipientCommunicator = communicationFromEntity[recipientEntity];

            var alliesWithinSpeakingRange = speakingRangeAlliesFromEntity[recipientEntity];
            var numAlliesWithinSpeakingRange = alliesWithinSpeakingRange.Length;
            for (var i = 0; i < numAlliesWithinSpeakingRange; i++)
            {
                var ally = alliesWithinSpeakingRange[i].entity;
                if(communicatedAllies.Contains(new AttackingCommunicatedAlliesElement
                {
                    ally = ally
                }))
                {
                    continue;
                }
                communicatedAllies.Add(new AttackingCommunicatedAlliesElement
                {
                    ally = ally
                });
                var newWaiter =
                    MessageFactory.CreateDelayedMessage(
                        ecb,
                        recipientEntity,
                        ally,
                        recipientCommunicator.verbalCommunicationTime
                    );
                Debug.Log("New waiter: " + newWaiter.ToString());
                ecb.AddComponent(newWaiter, new AttackingCommunicationWaiter
                {
                    tracker = trackerEntity
                });
                ecb.AddComponent( newWaiter, recipientBlackboard);
                ecb.AddComponent( newWaiter, recipientGlobalBlackboardData);
                trackedWaiters.Add(new AttackingCommunicationTrackedWaiterElement
                {
                    waiter = newWaiter
                });
            }

            ecb.AddComponent( waiterEntity, new MessageFinished());
        }
    }

Here is the code that schedules the above job as well as related jobs. Note that the same EntityCommandBuffer is used across all three. Maybe that’s related to the problem?

protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var ecb = endFrame.CreateCommandBuffer();
        var ecbConcurrent = ecb.ToConcurrent();
        var alliesFromEntity = GetBufferFromEntity<AllyWithinSpeakingRangeElement>(true);
        var communicatedAlliesFromEntity = GetBufferFromEntity< AttackingCommunicatedAlliesElement>();
        var blackboardFromEntity = GetComponentDataFromEntity< BlackboardType>();
        var globalBlackboardFromEntity = GetComponentDataFromEntity<GlobalAttackingBlackboard>();
        var communicationFromEntity = GetComponentDataFromEntity<AttackingCommunication>();
        var trackedWaitersFromEntity = GetBufferFromEntity<AttackingCommunicationTrackedWaiterElement>();
        var speakingRangeAlliesFromEntity = GetBufferFromEntity<AllyWithinSpeakingRangeElement>();
        var trackedWaitersFromEntityReadonly = GetBufferFromEntity<AttackingCommunicationTrackedWaiterElement>(true);

        inputDeps = new StartCommunicationJob
        {
            ecb = ecbConcurrent,
            alliesFromEntity = alliesFromEntity
        }.Schedule(this, inputDeps);

        inputDeps = new ReceiveCommunicationJob
        {
            ecb = ecb,
            communicationFromEntity = communicationFromEntity,
            communicatedAlliesFromEntity = communicatedAlliesFromEntity,
            blackboardFromEntity = blackboardFromEntity,
            globalBlackboardFromEntity = globalBlackboardFromEntity,
            trackedWaitersFromEntity = trackedWaitersFromEntity,
            speakingRangeAlliesFromEntity = speakingRangeAlliesFromEntity

        }.ScheduleSingle(this, inputDeps);
        inputDeps = new EndCommunicationJob
        {
            trackedWaitersFromEntity = trackedWaitersFromEntityReadonly,
            ecb = ecbConcurrent
        }.Schedule(this, inputDeps);

        endFrame.AddJobHandleForProducer(inputDeps);
        return inputDeps;
    }

ECB only creates temporary entities (with negative indices) until Playback(EntityManager) is called. You can’t store these.

Otherwise A) it wouldn’t work in jobs B) would create a sync point every time an entity was created.

3 Likes

That makes sense, but strangely enough the concurrent version of ECB is creating a valid entity, despite being used inside of a job

private struct StartCommunicationJob : IJobForEachWithEntity<AttackingCommunication, BlackboardType, GlobalAttackingBlackboard, ReadyMessage>
    {
        public EntityCommandBuffer.Concurrent ecb;
        [ReadOnly]
        public BufferFromEntity<AllyWithinSpeakingRangeElement> alliesFromEntity;

        public void Execute(
            Entity entity,
            int index,
            [ReadOnly] ref AttackingCommunication communicator,
            [ReadOnly] ref BlackboardType blackboard,
            [ReadOnly] ref GlobalAttackingBlackboard globalBlackboard,
            [ReadOnly] ref ReadyMessage message
        )
        {
            ecb.RemoveComponent<ReadyMessage>(index, entity);

            var tracker = ecb.CreateEntity(index);
            var newCommunicatedAllies = ecb.AddBuffer<AttackingCommunicatedAlliesElement>(index, tracker);
            newCommunicatedAllies.Add(new AttackingCommunicatedAlliesElement
            {
                ally = entity
            });
            var newTrackedWaiters = ecb.AddBuffer<AttackingCommunicationTrackedWaiterElement>(index, tracker);
            ecb.AddComponent(index, tracker, new AttackingCommunicationTrackerTag());

            var alliesWithinSpeakingRange = alliesFromEntity[entity].AsNativeArray();
            var numAlliesWithinSpeakingRange = alliesWithinSpeakingRange.Length;
            for(var i = 0; i < numAlliesWithinSpeakingRange; i++)
            {
                var ally = alliesWithinSpeakingRange[i].entity;
                newCommunicatedAllies.Add(new AttackingCommunicatedAlliesElement
                {
                    ally = ally
                });
                var newWaiter =
                    MessageFactory.CreateDelayedMessage(
                        ecb,
                        index,
                        entity,
                        ally,
                        communicator.verbalCommunicationTime
                    );
                ecb.AddComponent(index, newWaiter, new AttackingCommunicationWaiter
                {
                    tracker = tracker
                });
                ecb.AddComponent(index, newWaiter, blackboard);
                ecb.AddComponent(index, newWaiter, globalBlackboard);
                newTrackedWaiters.Add(new AttackingCommunicationTrackedWaiterElement
                {
                    waiter = newWaiter
                });
            }
        }
    }

This one is scheduled using Single, and newWaiter is always valid. Maybe it has something to do with the index provided?

It’s nothing to do with Concurrent, it still creates a negative entity.

unsafe public struct Concurrent
{
    public Entity CreateEntity(int jobIndex, EntityArchetype archetype = new EntityArchetype())
    {
       CheckWriteAccess();
       var chain = ThreadChain;
       // NOTE: Contention could be a performance problem especially on ARM
       // architecture. Maybe reserve a few indices for each job would be a better
       // approach or hijack the Version field of an Entity and store jobIndex
       int index = Interlocked.Decrement(ref m_Data->m_Entity.Index);
       m_Data->AddCreateCommand(chain, jobIndex, ECBCommand.CreateEntity,  index, archetype, kBatchableCommand);
       return new Entity {Index = index};
   }

You can see it’s simply decremented every time it’s called.

The magic is, these entities are automatically remapped for you when the real entity is created later.

1 Like

Yeah, you’re right, the entities created in StartCommunicationJob are indeed negative. The difference though is that they are added to a DynamicBuffer that was also created in the same job. As a result, when the same Entity is accessed in ReceiveCommunicationJob, it has a sensible index and version. Maybe there is some magic that resolves the DynamicBuffer, notices that the stored entities are temporary, and then resolves those entities too. @tertle do you know any alternatives for adding a wrapped entity that was just created to a DynamicBuffer inside a job?

Solved it. When a newWaiter is created, instead of adding to or removing from tracker, add a component to newWaiter called NeedsToBeTracked or NeedsToBeUntracked that stores the entity of the tracker (guaranteed to be existing at the time of execution). Two new jobs called TrackingJob and UntrackingJob will look for waiters with these components, and act upon the components’ trackers’ tracked waiters.

1 Like