InvalidOperationException: The previously scheduled job reads from the NativeArray

Hey guys, in one of my job I need to read LocalToWorld matrix, but another job writes to this LocalToWorld component. And I receive this exception.

InvalidOperationException: The previously scheduled job RaycastSystem:RaycastJob reads from the NativeArray RaycastJob.Data.localToWorldType. You are trying to schedule a new job TRSToLocalToWorldSystem:TRSToLocalToWorld, which writes to the same NativeArray (via TRSToLocalToWorld.Data.LocalToWorldType). To guarantee safety, you must include RaycastSystem:RaycastJob as a dependency of the newly scheduled job.

This situation actually happens pretty often and I really want to know how should I resolve it.
I understand that I may complete my job and everything will be fine, or I may add a dependency to my job, but in case with LocalToWorld, I have no idea how to get the completion handle. And I don’t like both these solutions.

What is the correct way to resolve it?

Show jobs/systems. Simply passing handles and proper queries should solve this (as that’s the entire point.)

1 Like

I have a custom raycast system, which works with LocalToWorld component. So, when I schedule RaycastJob I receive InvalidOperationException from my first comment.

using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
using BoxCollider = SE.Core.Collisions.BoxCollider;
using SphereCollider = SE.Core.Collisions.SphereCollider;

public struct IgnoreRaycast : IComponentData {}

public class RaycastSystem : JobComponentSystem  {
    [BurstCompile]
    private struct RaycastJob : IJobChunk {
        [ReadOnly] public ArchetypeChunkEntityType entityType;
        [ReadOnly] public ArchetypeChunkComponentType<LocalToWorld> localToWorldType;
        [ReadOnly] public ArchetypeChunkComponentType<BoxCollider> boxColliderType;
        [ReadOnly] public ArchetypeChunkComponentType<SphereCollider> sphereColliderType;

        public Ray ray;
        public NativeArray<Entity> closestEntityResult;
     
        [DeallocateOnJobCompletion]
        public NativeArray<float> closestDistanceResult;

        private static float3 TransformNormal(in float3 normal, in float4x4 matrix) {
            return new float3(
                normal.x * matrix.c0.x + normal.y * matrix.c1.x + normal.z * matrix.c2.x,
                normal.x * matrix.c0.y + normal.y * matrix.c1.y + normal.z * matrix.c2.y,
                normal.x * matrix.c0.z + normal.y * matrix.c1.z + normal.z * matrix.c2.z);
        }

        private bool hit_sphere(in float3 center, in float radius, in float3 ro, in float3 rd, out float distance) {
            distance = float.MaxValue;
            float3 oc = ro - center;
            float a = math.dot(rd, rd);
            float b = (float) 2.0 * math.dot(oc, rd);
            float c = math.dot(oc, oc) - radius * radius;
            float discriminant = b * b - 4 * a * c;
            if (discriminant < 0.0) {
                return false;
            } else {
                float numerator = -b - math.sqrt(discriminant);
                if (numerator > 0.0) {
                    distance = (float) (numerator / (2.0 * a));
                    return true;
                }

                numerator = -b + math.sqrt(discriminant);
                if (numerator > 0.0) {
                    distance = (float) (numerator / (2.0 * a));
                    return true;
                } else {
                    return false;
                }
            }
        }

        public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) {
            if (chunk.Has(boxColliderType)) {
                var entities = chunk.GetNativeArray(entityType);
                var chunkLocalToWorlds = chunk.GetNativeArray(localToWorldType);
                var chunkBoxColliders = chunk.GetNativeArray(boxColliderType);

                for (var i = 0; i < chunk.Count; i++) {
                    float4x4 worldToLocalMatrix = math.inverse(chunkLocalToWorlds[i].Value);

                    Ray localRay = ray;
                    localRay.origin = math.transform(worldToLocalMatrix, localRay.origin);
                    localRay.direction = math.normalize(TransformNormal(localRay.direction, worldToLocalMatrix));
                 
                    Bounds bounds = new Bounds(chunkBoxColliders[i].center, chunkBoxColliders[i].size);
                    if (bounds.IntersectRay(localRay, out float distance)) {
                        if (distance < closestDistanceResult[0]) {
                            closestDistanceResult[0] = distance;
                            closestEntityResult[0] = entities[i];
                        }
                    }
                }
            } else if (chunk.Has(sphereColliderType)) {
                var entities = chunk.GetNativeArray(entityType);
                var chunkLocalToWorlds = chunk.GetNativeArray(localToWorldType);
                var sphereColliders = chunk.GetNativeArray(sphereColliderType);
                for (var i = 0; i < chunk.Count; i++) {
                    float4x4 worldToLocalMatrix = math.inverse(chunkLocalToWorlds[i].Value);
                 
                    Ray localRay = ray;
                    localRay.origin = math.transform(worldToLocalMatrix, localRay.origin);
                    localRay.direction = math.normalize(TransformNormal(localRay.direction, worldToLocalMatrix));

                    if (hit_sphere(sphereColliders[i].center, sphereColliders[i].radius, localRay.origin, localRay.direction, out float distance)) {
                        if (distance < closestDistanceResult[0]) {
                            closestDistanceResult[0] = distance;
                            closestEntityResult[0] = entities[i];
                        }
                    }
                }
            }
        }
    }
 
    private EntityQuery m_group;
 
    protected override void OnCreate() {
        base.OnCreate();
        Enabled = false;
        m_group = GetEntityQuery(new EntityQueryDesc {
            All = new[] {
                ComponentType.ReadOnly<LocalToWorld>(),
            },
            Any = new[] {
                ComponentType.ReadOnly<BoxCollider>(),
                ComponentType.ReadOnly<SphereCollider>(),
            },
            None = new[] {
                ComponentType.ReadOnly<IgnoreRaycast>(),
            },
            Options = EntityQueryOptions.FilterWriteGroup
        });
    }

    public JobHandle Raycast(Ray ray, JobHandle inputDeps, NativeArray<Entity> closestEntityResult) {
        NativeArray<float> closestDistanceResult = new NativeArray<float>(1, Allocator.TempJob, NativeArrayOptions.UninitializedMemory) {[0] = float.MaxValue};
        JobHandle raycastJobHandle = new RaycastJob {
            entityType = GetArchetypeChunkEntityType(),
            localToWorldType = GetArchetypeChunkComponentType<LocalToWorld>(true),
            boxColliderType = GetArchetypeChunkComponentType<BoxCollider>(true),
            sphereColliderType = GetArchetypeChunkComponentType<SphereCollider>(true),
            closestEntityResult = closestEntityResult,
            closestDistanceResult = closestDistanceResult,
            ray = ray,
        }.Schedule(m_group, inputDeps);
        return raycastJobHandle;
    }
 
    protected override JobHandle OnUpdate(JobHandle inputDeps) {
        return inputDeps;
    }
}

SelectionSystem uses the RaycastSystem

using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Entities;
using Unity.Jobs;
using UnityEngine;

public struct Selected : IComponentData {
}

[AlwaysUpdateSystem]
public class SelectionSystem : JobComponentSystem {
    private struct SelectJob : IJob {
#pragma warning disable 649
        [NativeSetThreadIndex] int threadId;
#pragma warning restore 649
        public EntityCommandBuffer.Concurrent entityCommandBuffer;
     
        [DeallocateOnJobCompletion]
        public NativeArray<Entity> closestEntityResult;
        public void Execute() {
            if (closestEntityResult[0] != Entity.Null) {
                entityCommandBuffer.AddComponent(threadId, closestEntityResult[0], new Selected());
            }
        }
    }
 
    private EndSimulationEntityCommandBufferSystem m_endSimulationEntityCommandBufferSystem;
    private RaycastSystem m_raycastSystem;
 
    protected override void OnCreate() {
        base.OnCreate();
        m_endSimulationEntityCommandBufferSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
        m_raycastSystem = World.GetOrCreateSystem<RaycastSystem>();
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps) {
        if (MouseInput.leftButtonWasPressed) {
            EntityCommandBuffer.Concurrent removeSelectedJobCommandBuffer = m_endSimulationEntityCommandBufferSystem.CreateCommandBuffer().ToConcurrent();
            EntityCommandBuffer.Concurrent selectJobCommandBuffer = m_endSimulationEntityCommandBufferSystem.CreateCommandBuffer().ToConcurrent();

            NativeArray<Entity> closestEntityResult = new NativeArray<Entity>(1, Allocator.TempJob, NativeArrayOptions.UninitializedMemory){[0] = Entity.Null};
         
            JobHandle removeSelectedComponentsHandle = Entities.ForEach((Entity entity, int entityInQueryIndex, in Selected selected) => { removeSelectedJobCommandBuffer.RemoveComponent<Selected>(entityInQueryIndex, entity); }).Schedule(inputDeps);

            Ray ray = CameraAccessor.camera.ScreenPointToRay(MouseInput.position);
            JobHandle raycastJobHandle = m_raycastSystem.Raycast(ray, removeSelectedComponentsHandle, closestEntityResult);

            JobHandle selectJobHandle = new SelectJob {
                entityCommandBuffer = selectJobCommandBuffer,
                closestEntityResult = closestEntityResult,
            }.Schedule(raycastJobHandle);
          
m_endSimulationEntityCommandBufferSystem.AddJobHandleForProducer(removeSelectedComponentsHandle);
m_endSimulationEntityCommandBufferSystem.AddJobHandleForProducer(selectJobHandle);
            return selectJobHandle;
        }
        return inputDeps;
    }
}

Maybe it is not the shortest example, but the Idea that I use LocalToWorld, and don’t know how to work with it properly without calling JobHandle.Complete()

Oh. You’re completely bypass the job/safety system by using m_raycastSystem = World.GetOrCreateSystem();

You really shouldn’t do this, but if you insist you are responsible for manually handling job dependencies.

Pretty much the issue is your system SelectionSystem has no dependency on LocalToWorld so the handle being passed in isn’t the correct one when the system is scheduled.

1 Like

yeah, but how should it be ideally? for me it absolutely unclear what should I do to be able to use safely LocalToWorld, or Translation or whatever in my jobs?

Maybe extract your raycast work from system and use it like utility and schedule it where you need (you can wrap all boilerplate code inside utility method and pass system to it and collect all ACCT etc.inside for exclude manual work every time)

1 Like

Oh, now I understood, I was really completely wrong with using RaycastSystem. I’ve done as @eizenhorn said, and it works. Thank both of you a lot!

The only thing that I don’t really like is GetEntityQuery is a protected method, and I can’t create a query inside of my RaycastUtils. So, I created it in SelectionSystem and it looks ugly :slight_smile: Is it the only approach to create EntityQuery in my case?

Here is what I have now

using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
using BoxCollider = SE.Core.Collisions.BoxCollider;
using SphereCollider = SE.Core.Collisions.SphereCollider;

public struct IgnoreRaycast : IComponentData {}

public static class RaycastUtils  {
    [BurstCompile]
    private struct RaycastJob : IJobChunk {
        [ReadOnly] public ArchetypeChunkEntityType entityType;
        [ReadOnly] public ArchetypeChunkComponentType<LocalToWorld> localToWorldType;
        [ReadOnly] public ArchetypeChunkComponentType<BoxCollider> boxColliderType;
        [ReadOnly] public ArchetypeChunkComponentType<SphereCollider> sphereColliderType;

        public Ray ray;
        public NativeArray<Entity> closestEntityResult;
      
        [DeallocateOnJobCompletion]
        public NativeArray<float> closestDistanceResult;

        private static float3 TransformNormal(in float3 normal, in float4x4 matrix) {
            return new float3(
                normal.x * matrix.c0.x + normal.y * matrix.c1.x + normal.z * matrix.c2.x,
                normal.x * matrix.c0.y + normal.y * matrix.c1.y + normal.z * matrix.c2.y,
                normal.x * matrix.c0.z + normal.y * matrix.c1.z + normal.z * matrix.c2.z);
        }

        private bool hit_sphere(in float3 center, in float radius, in float3 ro, in float3 rd, out float distance) {
            distance = float.MaxValue;
            float3 oc = ro - center;
            float a = math.dot(rd, rd);
            float b = (float) 2.0 * math.dot(oc, rd);
            float c = math.dot(oc, oc) - radius * radius;
            float discriminant = b * b - 4 * a * c;
            if (discriminant < 0.0) {
                return false;
            } else {
                float numerator = -b - math.sqrt(discriminant);
                if (numerator > 0.0) {
                    distance = (float) (numerator / (2.0 * a));
                    return true;
                }

                numerator = -b + math.sqrt(discriminant);
                if (numerator > 0.0) {
                    distance = (float) (numerator / (2.0 * a));
                    return true;
                } else {
                    return false;
                }
            }
        }

        public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) {
            if (chunk.Has(boxColliderType)) {
                var entities = chunk.GetNativeArray(entityType);
                var chunkLocalToWorlds = chunk.GetNativeArray(localToWorldType);
                var chunkBoxColliders = chunk.GetNativeArray(boxColliderType);

                for (var i = 0; i < chunk.Count; i++) {
                    float4x4 worldToLocalMatrix = math.inverse(chunkLocalToWorlds[i].Value);

                    Ray localRay = ray;
                    localRay.origin = math.transform(worldToLocalMatrix, localRay.origin);
                    localRay.direction = math.normalize(TransformNormal(localRay.direction, worldToLocalMatrix));
                  
                    Bounds bounds = new Bounds(chunkBoxColliders[i].center, chunkBoxColliders[i].size);
                    if (bounds.IntersectRay(localRay, out float distance)) {
                        if (distance < closestDistanceResult[0]) {
                            closestDistanceResult[0] = distance;
                            closestEntityResult[0] = entities[i];
                        }
                    }
                }
            } else if (chunk.Has(sphereColliderType)) {
                var entities = chunk.GetNativeArray(entityType);
                var chunkLocalToWorlds = chunk.GetNativeArray(localToWorldType);
                var sphereColliders = chunk.GetNativeArray(sphereColliderType);
                for (var i = 0; i < chunk.Count; i++) {
                    float4x4 worldToLocalMatrix = math.inverse(chunkLocalToWorlds[i].Value);
                  
                    Ray localRay = ray;
                    localRay.origin = math.transform(worldToLocalMatrix, localRay.origin);
                    localRay.direction = math.normalize(TransformNormal(localRay.direction, worldToLocalMatrix));

                    if (hit_sphere(sphereColliders[i].center, sphereColliders[i].radius, localRay.origin, localRay.direction, out float distance)) {
                        if (distance < closestDistanceResult[0]) {
                            closestDistanceResult[0] = distance;
                            closestEntityResult[0] = entities[i];
                        }
                    }
                }
            }
        }
    }

    public static JobHandle Raycast(ComponentSystemBase system, EntityQuery group, Ray ray, JobHandle inputDeps, NativeArray<Entity> closestEntityResult) {
        NativeArray<float> closestDistanceResult = new NativeArray<float>(1, Allocator.TempJob, NativeArrayOptions.UninitializedMemory) {[0] = float.MaxValue};
        JobHandle raycastJobHandle = new RaycastJob {
            entityType = system.GetArchetypeChunkEntityType(),
            localToWorldType = system.GetArchetypeChunkComponentType<LocalToWorld>(true),
            boxColliderType = system.GetArchetypeChunkComponentType<BoxCollider>(true),
            sphereColliderType = system.GetArchetypeChunkComponentType<SphereCollider>(true),
            closestEntityResult = closestEntityResult,
            closestDistanceResult = closestDistanceResult,
            ray = ray,
        }.Schedule(group, inputDeps);
        return raycastJobHandle;
    }
}

And SelectionSystem

using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Entities;
using Unity.Jobs;
using Unity.Transforms;
using UnityEngine;
using BoxCollider = SE.Core.Collisions.BoxCollider;
using SphereCollider = SE.Core.Collisions.SphereCollider;

public struct Selected : IComponentData {
}

[AlwaysUpdateSystem]
public class SelectionSystem : JobComponentSystem {
    private struct SelectJob : IJob {
#pragma warning disable 649
        [NativeSetThreadIndex] int threadId;
#pragma warning restore 649
        public EntityCommandBuffer.Concurrent entityCommandBuffer;
      
        [DeallocateOnJobCompletion]
        public NativeArray<Entity> closestEntityResult;
        public void Execute() {
            if (closestEntityResult[0] != Entity.Null) {
                entityCommandBuffer.AddComponent(threadId, closestEntityResult[0], new Selected());
            }
        }
    }
  
    private EndSimulationEntityCommandBufferSystem m_endSimulationEntityCommandBufferSystem;
    private EntityQuery m_group;
  
    protected override void OnCreate() {
        base.OnCreate();
        m_endSimulationEntityCommandBufferSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
        m_group = GetEntityQuery(new EntityQueryDesc {
            All = new[] {
                ComponentType.ReadOnly<LocalToWorld>(),
            },
            Any = new[] {
                ComponentType.ReadOnly<BoxCollider>(),
                ComponentType.ReadOnly<SphereCollider>(),
            },
            None = new[] {
                ComponentType.ReadOnly<IgnoreRaycast>(),
            },
            Options = EntityQueryOptions.FilterWriteGroup
        });
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps) {
        if (MouseInput.leftButtonWasPressed) {
            EntityCommandBuffer.Concurrent removeSelectedJobCommandBuffer = m_endSimulationEntityCommandBufferSystem.CreateCommandBuffer().ToConcurrent();
            EntityCommandBuffer.Concurrent selectJobCommandBuffer = m_endSimulationEntityCommandBufferSystem.CreateCommandBuffer().ToConcurrent();

            NativeArray<Entity> closestEntityResult = new NativeArray<Entity>(1, Allocator.TempJob, NativeArrayOptions.UninitializedMemory){[0] = Entity.Null};
          
            JobHandle removeSelectedComponentsHandle = Entities.ForEach((Entity entity, int entityInQueryIndex, in Selected selected) => {
                removeSelectedJobCommandBuffer.RemoveComponent<Selected>(entityInQueryIndex, entity);
            }).Schedule(inputDeps);

            Ray ray = CameraAccessor.camera.ScreenPointToRay(MouseInput.position);
            JobHandle raycastJobHandle = RaycastUtils.Raycast(this, m_group, ray, removeSelectedComponentsHandle, closestEntityResult);

            JobHandle selectJobHandle = new SelectJob {
                entityCommandBuffer = selectJobCommandBuffer,
                closestEntityResult = closestEntityResult,
            }.Schedule(raycastJobHandle);
            m_endSimulationEntityCommandBufferSystem.AddJobHandleForProducer(removeSelectedComponentsHandle);
            m_endSimulationEntityCommandBufferSystem.AddJobHandleForProducer(selectJobHandle);
            return selectJobHandle;
        }
        return inputDeps;
    }
}

Take a look at how weird creation EntityQuery in SelectionSystem looks.

You can also get an entity query through an entity manager. You can acccess the default one globally via World.DefaultGameObjectInjectionWorld.EntityManager

https://docs.unity3d.com/Packages/com.unity.entities@0.7/manual/ecs_entity_query.html?q=CreateEntityQuery#creating-a-entityquery

1 Like

Exactly what I was looking for! Thank you.

It’s not like Get, it’s Create :slight_smile: This query will be outside ReaderWriter safety chain, because it miss adding dependencies like GetEntityQuery do.
5557459--572662--upload_2020-3-6_12-36-15.png
Also does many unnecessery work and you should cache it by yourself and not create every time. If you inside system you should always use GetEntityQuery.
Correct me if I’m wrong :slight_smile:

2 Likes

Probably you’re right but it works without any problem for me. So, I just moved entity query to RaycastUtils

private static EntityQuery m_group;
private static EntityQuery m_groupLazy {
    get {
        if (m_group == null) {
            m_group = World.DefaultGameObjectInjectionWorld.EntityManager.CreateEntityQuery(new EntityQueryDesc {
                All = new[] {
                    ComponentType.ReadOnly<LocalToWorld>(),
                },
                Any = new[] {
                    ComponentType.ReadOnly<BoxCollider>(),
                    ComponentType.ReadOnly<SphereCollider>(),
                },
                None = new[] {
                    ComponentType.ReadOnly<IgnoreRaycast>(),
                },
                Options = EntityQueryOptions.FilterWriteGroup
            });
        }
        return m_group;
    }
}

[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void Init() {
    m_group = null;
}