Unity DOTS: Randomly Accessing Component Data in Jobs

Summary

Use ComponentDataFromEntity and BufferFromEntity to randomly access component data in jobs. However, random access results in more cache misses.

Environment

  • Unity 2019.4.0f1
  • Entities 0.11.1-preview.4
  • Burst 1.3.0-preview.12

Explanation

If you try to access component data from other entities in a scheduled job, even with WithReadOnly() capturing entityManager:

public struct SampleComponent : IComponentData
{
    public int Value;
}
public class SampleSystem : JobComponentSystem
{
    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        EntityManager entityManager = EntityManager;
        JobHandle job = Entities
            .WithReadOnly(entityManager)
            .ForEach((Entity entity) =>
        {
            if (entityManager.HasComponent<SampleComponent>(entity))
            {
                SampleComponent sampleComp = entityManager.GetComponentData<SampleComponent>(entity);
                Debug.Log($"{entity} has SampleComponent with value = {sampleComp.Value}");
            }
        }).Schedule(inputDeps);

        return job;
    }
}

After hitting play, lots of error logs will show up:

InvalidOperationException: The previously scheduled job SampleSystem:<>c__DisplayClass_OnUpdate_LambdaJob0 reads from the NativeArray <>c__DisplayClass_OnUpdate_LambdaJob0.JobData.entityManager. You must call JobHandle.Complete() on the job SampleSystem:<>c__DisplayClass_OnUpdate_LambdaJob0, before you can write to the NativeArray safely.

The cause of the error is that both HasComponent() and GetComponentData() somehow invoke GetCheckedEntityDataAccess(), which checks write access, but I’m not sure why, since I consider HasComponent() and GetComponentData() not having the intention to write.

public bool HasComponent<T>(Entity entity)
{
    return GetCheckedEntityDataAccess()->HasComponent(entity, ComponentType.ReadWrite<T>());
}
public T GetComponentData<T>(Entity entity) where T : struct, IComponentData
{
    var ecs = GetCheckedEntityDataAccess();
    return ecs->GetComponentData<T>(entity);
}

And they both invoke GetCheckedEntityDataAccess():

internal EntityDataAccess* GetCheckedEntityDataAccess()
{
#if ENABLE_UNITY_COLLECTIONS_CHECKS
    AtomicSafetyHandle.CheckWriteAndThrow(m_Safety);
    if (m_JobMode != m_EntityDataAccess->m_JobMode)
    {
        throw new InvalidOperationException($"EntityManager cannot be used from this context job mode {m_JobMode} != current mode {m_EntityDataAccess->m_JobMode}");
    }
#endif
    return m_EntityDataAccess;
}

Instead, we need to use ComponentDataFromEntity to random access components from other entities:

public class SampleSystem : JobComponentSystem
{
    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        ComponentDataFromEntity<SampleComponent> allSampleComps = GetComponentDataFromEntity<SampleComponent>(true);
        JobHandle job = Entities
            .WithReadOnly(allSampleComps)
            .ForEach((Entity entity) =>
        {
            if (allSampleComps.Exists(entity))
            {
                SampleComponent sampleComp = allSampleComps[entity];
                Debug.Log($"{entity} has SampleComponent with value = {sampleComp.Value}");
            }
        }).Schedule(inputDeps);

        return job;
    }
}

An overhead should be noted is that random access results in more cache misses. It may be acceptable if total amount of the components is rather few.

Besides, there is BufferFromEntity, too, which is for DynamicBuffer.

Leave a Comment