BufferFromEntity/ComponentDataFromEntity safe in IJob/IJobParallelFor?

This came up in another post I made and an official answer would be great because I’m not 100% clear from reading the source.

In this example, does the ECS safety system correctly understand that SystemB.BJob has a dependency on the Buffer type via BufferFromEntity, and thus SystemC.CJob waits on it before running?

public struct Buffer : IBufferElementData
{
    int x;
}

public struct Data : IComponentData
{
    int y;
}

public class SystemA : JobComponentSystem
{
    public struct AJob : IJobForEachWithEntity<Data>
    {
        public BufferFromEntity<Buffer> buffers;
        public void Execute(Entity entity, int index, ref Data data)
        {
            var buffer = buffers[entity];
            // do some work, mutate buffer
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        return (new AJob{
            buffers = GetBufferFromEntity<Buffer>(isReadOnly:false),
        }).Schedule(this, inputDeps);
    }
}

[UpdateAfter(typeof(SystemA))]
public class SystemB : JobComponentSystem
{
    public struct BJob : IJobParallelFor
    {
        [ReadOnly] public NativeArray<Entity> entities;
        [ReadOnly] public BufferFromEntity<Buffer> buffers;
    
        public void Execute(int index)
        {
            var entity = entities[index];
            var buffer = buffers[entity];
            // do some work
        }
    }
 
    private EntityQuery query;
 
    protected override void OnCreate()
    {
        query = GetEntityQuery(
            ComponentType.ReadOnly<Data>(),
            ComponentType.ReadOnly<Buffer>()
        );
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var entities = query.ToEntityArray(Allocator.TempJob, out var entitiesHandle);
        var buffers = GetBufferFromEntity<Buffer>(isReadOnly:true);
    
        // Does the ECS safety system understand that this job has a dependency on a read-only
        // BufferFromEntity<Buffer>, despite it being a job that is not a IJobForEach or IJobChunk?
        return (new BJob{
            entities = entities,
            buffers = buffers,
        }).Schedule(entities.Length, 1, JobHandle.CombineDependencies(inputDeps entitiesHandle));
    }
}

[UpdateAfter(typeof(SystemB))]
public class SystemC : JobComponentSystem
{
    public struct CJob : IJobForEachWithEntity<Data>
    {
        public BufferFromEntity<Buffer> buffers;
        public void Execute(Entity entity, int index, ref Data data)
        {
            var buffer = buffers[entity];
            // do some work, mutate buffer
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        // Does the safety system know that it must wait for SystemB.BJob to finish before
        // this is allowed to run?
        return (new CJob{
            buffers = GetBufferFromEntity<Buffer>(isReadOnly:false),
        }).Schedule(this, inputDeps);
    }
}

Also I have tried to use the [NativeDisable*] attributes to make it safe to change the BJob.buffers from read-only to safely writeable in parallel inside of an IJob/IJobParallelFor (and changing isReadOnly to true in the OnUpdate method), but the safety system complains that it is not allowed (despite never writing/accessing the same buffer across concurrently scheduled jobs).

As long as you only use the entity passed in by IJobForEachWithEntity then what you are doing is safe. You might need

[NativeDisableParallelForRestriction]

though to stop safety warnings. Note if you refactor your code at some point and use any entity other than the one passed in via the job it will not be safe and you will not be warned.

In this example it’s BJob I am asking about. It is not using IJobForEachWithEntity (assume that in real code there is a purpose for doing this and that it cannot be simply switched to using IJobForEachWithEntity). And for the purposes of this question, treat the use of the BufferFromEntity in BJob as read-only.

Checking the source in Unity.Entities/ComponentSystem.cs:716 the GetBufferFromEntity calls AddReaderWriter who then check if the dependency needs completion. I think one can assume this will force a sync point in the system, forcing the main thread to wait even before the scheduling.

[ ]'s

Sorry I was on phone and didn’t even see that I could scroll down.

The safety system uses the query and GetBufferFromEntity(isReadOnly:true), not the job itself.

So yes that is completely safe and the intention of the safety system.

1 Like

Calling GetBufferFromEntity/GetComponentDataFromEntity doesn’t force a sync just FYI. Accessing data within them would however.

Ok that’s what I was thinking after digging deep into the source. Wanted to make sure I wasn’t off track.

I still have the second issue though that I am unable to get mutable access to a DynamicBuffer from a BufferFromEntity in an IJob or IJobParallelFor. Despite ensuring that my concurrent jobs in this scenario never read/write to the same entity’s DynamicBuffer, the safety system blows up at me. I do not have the same problem when I do this with ComponentDataFromEntity though.

[NativeDisableParallelForRestriction]

Ok here is an example reproducing the issue I’m talking about:

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

namespace Game
{
    [UpdateInGroup(typeof(SimulationSystemGroup))]
    public class TestSystem : JobComponentSystem
    {
        public struct Data1 : IComponentData { public int i, version; }

        [InternalBufferCapacity(0)]
        public struct Buffer1 : IBufferElementData { public int i, j, version; }

        private EntityQuery query;

        protected override void OnCreate()
        {
            query = GetEntityQuery(
                ComponentType.ReadWrite<Data1>(),
                ComponentType.ReadWrite<Buffer1>()
            );

            for (int i = 0; i < 200; i++) {
                var e = EntityManager.CreateEntity();
                EntityManager.AddComponentData(e, new Data1{ i = i });
                var b = EntityManager.AddBuffer<Buffer1>(e);
                for (int j = 0; j < 25; j++) {
                    b.Add(new Buffer1{ i = i, j = j });
                }
            }
        }

        private struct MutateJob : IJob
        {
            public int index;
            [ReadOnly][DeallocateOnJobCompletion] public NativeArray<Entity> entities;

            // Works fine, no safety warnings
            [NativeDisableContainerSafetyRestriction] public ComponentDataFromEntity<Data1> datas;

            // Blows up, gives this error:
            //
            // InvalidOperationException: The previously scheduled job TestSystem:MutateJob writes
            // to the NativeArray MutateJob.buffers. You are trying to schedule a new job
            // TestSystem:MutateJob, which writes to the same NativeArray (via MutateJob.buffers).
            // To guarantee safety, you must include TestSystem:MutateJob as a dependency of the newly
            // scheduled job.
            [NativeDisableContainerSafetyRestriction] public BufferFromEntity<Buffer1> buffers;

            public void Execute()
            {
                // Only references the data at index, never touches another entity which would cause
                // a race and safety system blow up.
                var entity = entities[index];
                var data = datas[entity];
                data.version++;
                datas[entity] = data;

                var buffer = buffers[entity];
                for (int i = 0; i < buffer.Length; i++) {
                    var d = buffer[i];
                    d.version++;
                    buffer[i] = d;
                }
            }
        }

        protected override JobHandle OnUpdate(JobHandle inputDeps)
        {
            var entities = query.ToEntityArray(Allocator.TempJob, out var entitiesHandle);
            var handles = new NativeArray<JobHandle>(entities.Length, Allocator.Temp);
            var datas = GetComponentDataFromEntity<Data1>(isReadOnly:false);
            var buffers = GetBufferFromEntity<Buffer1>(isReadOnly:false);

            inputDeps = JobHandle.CombineDependencies(inputDeps, entitiesHandle);

            // Individual job only ever references one entity's data.
            for (int i = 0; i < entities.Length; i++) {
                handles[i] = (new MutateJob{
                    index = i,
                    entities = entities,
                    datas = datas,
                    buffers = buffers,
                }).Schedule(inputDeps);
            }

            var jobHandle = JobHandle.CombineDependencies(handles);
            handles.Dispose();
            return jobHandle;
        }
    }
}

Interestingly, switching to a single IJobParallelFor and forcing max concurrency it works fine:

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

namespace Game
{
    [UpdateInGroup(typeof(SimulationSystemGroup))]
    public class TestSystem : JobComponentSystem
    {
        public struct Data1 : IComponentData { public int i, version; }

        [InternalBufferCapacity(0)]
        public struct Buffer1 : IBufferElementData { public int i, j, version; }

        private EntityQuery query;

        protected override void OnCreate()
        {
            query = GetEntityQuery(
                ComponentType.ReadWrite<Data1>(),
                ComponentType.ReadWrite<Buffer1>()
            );

            for (int i = 0; i < 200; i++) {
                var e = EntityManager.CreateEntity();
                EntityManager.AddComponentData(e, new Data1{ i = i });
                var b = EntityManager.AddBuffer<Buffer1>(e);
                for (int j = 0; j < 25; j++) {
                    b.Add(new Buffer1{ i = i, j = j });
                }
            }
        }

        private struct MutateJob : IJobParallelFor
        {
            [ReadOnly][DeallocateOnJobCompletion] public NativeArray<Entity> entities;

            // Works fine, no safety warnings
            [NativeDisableContainerSafetyRestriction] public ComponentDataFromEntity<Data1> datas;

            // Works fine now
            [NativeDisableContainerSafetyRestriction] public BufferFromEntity<Buffer1> buffers;

            public void Execute(int index)
            {
                var entity = entities[index];
                var data = datas[entity];
                data.version++;
                datas[entity] = data;

                var buffer = buffers[entity];
                for (int i = 0; i < buffer.Length; i++) {
                    var d = buffer[i];
                    d.version++;
                    buffer[i] = d;
                }
            }
        }

        protected override JobHandle OnUpdate(JobHandle inputDeps)
        {
            var entities = query.ToEntityArray(Allocator.TempJob, out var entitiesHandle);
            var handles = new NativeArray<JobHandle>(entities.Length, Allocator.Temp);
            var datas = GetComponentDataFromEntity<Data1>(isReadOnly:false);
            var buffers = GetBufferFromEntity<Buffer1>(isReadOnly:false);
         
            inputDeps = JobHandle.CombineDependencies(inputDeps, entitiesHandle);

            var jobHandle = (new MutateJob{
                entities = entities,
                datas = datas,
                buffers = buffers,
            }).Schedule(entities.Length, 1, inputDeps);

            return jobHandle;
        }
    }
}

Seems like the IJob use case issue is a bug? Or just a limitation in how that attribute can function. shrug

It’s unfortunate if this limitation remains, because it makes it impossible to add per-entity temporary data (like a NativeQueue/List/etc) when you need them to do intermediate calculations (can’t have an array of native containers).

That doesn’t fix the issue that my first example demonstrates unfortunately. You get the same exception.

About your first example. I’m not in bed now and not on phone and with this thread and with previous (where we discussed yesterday) I can see your case now :slight_smile: In first case you can’t disable safety restrictions. You can process same container inside one parallel job with [NativeDisable*] attributes (IJobParallelFor for example) if you guarantee safety. But you can’t process same container in different jobs in parallel even if you can guarantee safety. You only can chain them.

:slight_smile: Let’s see where this goes, but yeah, I am with you…

possibly language get’s in the way sometimes (not native)

from other thread:
#4 me: Parallel write from multiple Jobs is not supported…
#5 him: Parallel write to most containers is definitely supported…

Was going through the Unity-Technologies/multiplayer project on GitHub and stumbled across this:

// FIXME: disable safety for BufferFromEntity is not working
serialDep[con+1] = serializeJob.Schedule(serialDep[con]);

Seems as though this is a bug that they are aware of.

1 Like

Nice find. Hoping this is the case. I also dream of a day when I can finally allocate native container memory from within a bursted job.

I thought one could create containers in a burst job so long as they are allocated using Allocator.TempJob?

Yes, this works since a few versions back, but the allocator is temp

You can create containers in regular jobs via Allocator.Temp, but as far as I am aware it is not supported in Burst yet.