Using Native Collections in IComponentData – Best Practices?

Hello everyone,

One thing I find missing in DOTS learning resources is a proper explanation of using native collections in IComponentData. Can (and should?) we put native collections directly into components?

Let’s consider the following example. First, we define component with native container, for example:

public struct CollectionsTest : IComponentData
{
    public NativeParallelHashMap<int, Entity> FilteredEntities;
}

Now, my idea for this component usage is simple:

  1. On game startup, initialize the container
  2. Regular game loop, fill this component with the data in the system number X, in my case it will be processed data from ICollisionEvents job
  3. Regular game loop, process previously obtained data in the system number X + 10, clear the container after processing
  4. Repeat steps 2 and 3 in every frame
  5. On game exit - dispose the container

In order to achieve points 1 and 5 I need to do it in the system in runtime, as we cannot allocate native containers in authorings:

public partial struct CollectionsHandlingSystem : ISystem
{
    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        Entity entity = state.EntityManager.CreateEntity();
        state.EntityManager.AddComponentData(entity, new CollectionsTest
        {
            FilteredEntities = new NativeParallelHashMap<int, Entity>(2048, Allocator.Persistent)
        });
    }

    [BurstCompile]
    public void OnDestroy(ref SystemState state)
    {
        CollectionsTest ct = SystemAPI.GetSingleton<CollectionsTest>();
        ct.FilteredEntities.Dispose();
    }
}

My questions:

  • Is this a valid approach?
  • I intend to use this only for singleton components - are there any potential issues with this method?
  • What pitfalls should I be aware of if I proceed this way?
  • In case that this is not the way to go, could you propose better solution for my usecase?

Thanks in advance!

Technically, you can put them in component. However it’s not practical because:

  1. You can NOT bake them.
  2. You can NOT access them in IJobEntity/IJobChunk the same way you access normal components.

The reasons:

  1. Baking system doesn’t support serializing native collections. What you have after baking is an integer represents the memory address at the time the collection was allocated on your machine. At runtime (either you enter play mode or on user’s machine) that would turn into an invalid memory address and can crash the game.
  2. The safety system doesn’t support native collections nested inside components. So jobs cannot reliably access them.

What you should do instead, either:

  1. Use DynamicBuffer.
  2. Put native containers on singleton components, retrieve them on main thread (OnUpdate) then pass them to the jobs.
  3. If you really want it, you can use the Unsafe- counterparts of Native-. But then you’re on your own, safety system cannot help you. And beware the nature of structs!
2 Likes

Yes, it is a valid approach. But remember: you can only treat CollectionsTest as a singleton component. Never forget that and you’ll be fine.

1 Like

Thanks for the guidance, @Laicasaane! It is more or less clear to me now, just let me elaborate on your points above:

regarding 2:
My current use case is:
a) collect list (NativeList in IComponentData) of collision events in system X
b) in system X+1 process this list in parallel

To be able to process events in system X+1 I had to use IJobParrallelForDefer, using basic IJobParrallelFor was not working as system state.Depency is not automatically tracking native collections inside the components, if I got it right.

So I guess manual dependency handling is another thing I should be aware of when using native collections on components - am I right?

regarding 3:
Could you come up with some example when using Unsafe collections is beneficial/needed? I understand that these types don’t have safety system, but I’m not sure if I get it’s practical usages.

And that comes to my last question - let’s say I am using unsafe list (or I simply disable safety checks on native collection/componentLookup) and I get race condition - for example writing to the same array element at the same time - will this produce some actual error to the console, eventually will my game crash?

Thank you very much for helping, means a lot for me!

No need. For example:

var listFromComp = SystemAPI.GetSingleton<CollectionComponent>().list;

state.Dependency = new ReadFromListJob() {
    list = listFromComp,
}.Schedule();

The safety system will automatically handle this for you the same way it handles other collections retrieved from somewhere else then passed into the job this same way.

I’m afraid I had never gone extreme to incorporate such usage into my code. More knowledgable people might be able to answer you better. You can seek them at the DOTS channel inside Unity Discord server.

Seek those people, they can tell you more on this. But you can always run some experiments to grasp the consequence I guess. The simplest one is scheduling 2 jobs running in parallel to each other, each writing to the same collection.

Well, isn’t this quote from documentation implying the opposite?

The Dependency property doesn’t track the dependencies that a job might have on data passed through a NativeArray or other similar containers. If you write a NativeArray in one job, and read that array in another, you must manually add the JobHandle of the first job as a dependency of the second. You can use JobHandle.CombineDependencies to do this.

Are you sure you understand that quote correctly? Because it’s about dependency between jobs. Not the safety mechanism when accessing a collection. I’ve been discussing only the later.

So if you need to pass the collection into 2 jobs of course you have to pass the JobHandle returned from job1.Schedule into job2.Schedule. This doesn’t contradict what I said though.

var handle1 = new Job1 {
    list = listFromComp
}.Schedule(state.Dependency);

state.Dependency = new Job2 {
    list = listFromComp
}.Schedule(handle1);

If you look into the source code of Native- collections, you will see there are safety checks. They do not require JobHandle to function though.

There is no safety check for Unsafe- counterparts.

1 Like

Some discussions I quickly find on Discord

https://discord.com/channels/489222168727519232/1064581837055348857/1305990679335407616

https://discord.com/channels/489222168727519232/1064581837055348857/1295967153077489695

https://discord.com/channels/489222168727519232/1064581837055348857/1340857514563993621

1 Like

Note that when you use this SystemAPI.GetSingleton, or anything from SystemAPI, your code will be rewritten to include additional EntityQuerys and JobHandles.

SystemAPI methods are just stubs, empty methods, act as a marker for source generator. Because of technical limitations, you can only use SystemAPI inside systems. The code written by you is not the actual code that will run at runtime.

For example,

public void OnUpdate(ref SystemState state)
{
    var listFromComp = SystemAPI.GetSingleton<CollectionComponent>().list;

    state.Dependency = new ReadFromListJob() {
        list = listFromComp,
    }.Schedule();
}

This OnUpdate never run. Instead the generated version whose name is somewhat similar to __OnUpdate_123456 will be the one that actually runs.

You can use “Go to declaration” function of your IDE to inspect these generated code.

1 Like

Thanks for all the info, @Laicasaane. I have never read much through codegen, but it can definitely help to understand what is going on under the hood.

I did some attempts with my code in the meantime and I have a simple example where I can demonstrate where exactly I am lacking my knowledge:

Prerequisite:

  • I know the following example can be done easily with DynamicBuffers, but as I want to understand native collections in IComponentData handling I will just use NativeList for the learning purposes and my problem demonstration

Component:

    public struct CollectionTestSingleton : IComponentData
    {
        public NativeList<Entity> EntityList;
    }

Initialization, disposal of the list:
In the dedicated system, see my first post

Actual problem:
We have system 1 where I want to fill EntityList with values:

    partial struct CollectionsTestingSystem1 : ISystem
    {
        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            CollectionTestSingleton singletonTest = SystemAPI.GetSingleton<CollectionTestSingleton>();
            state.Dependency = new WriteElementsToCollection()
            {
                EntitiesList = singletonTest.EntityList
            }
            .Schedule(state.Dependency);
        }

        [BurstCompile]
        public struct WriteElementsToCollection : IJob
        {
            public NativeList<Entity> EntitiesList;
            
            public void Execute()
            {
                for (int i = 0; i < 10; i++)
                {
                    EntitiesList.Add(Entity.Null);
                }
            }
        }
    }

And then we have system 2 where I want to read EntityList and clear the list. This system runs right after previous system 1:

    [UpdateAfter(typeof(CollectionsTestingSystem1))]
    partial struct CollectionsTestingSystem2 : ISystem
    {
        [BurstCompile]
        public void OnUpdate(ref SystemState state)
        {
            CollectionTestSingleton singletonTest = SystemAPI.GetSingleton<CollectionTestSingleton>();
        
            //job 1 - reading from the list
            state.Dependency = new ReadElementsFromCollection()
            {
                EntitiesList = singletonTest.EntitiesList.AsDeferredJobArray()
            }
            .Schedule(pce.EntitiesList, 3, state.Dependency);

             //job 2 - clearing the list
            state.Dependency = new ClearStorageLists()
            {
                EntitiesList = singletonTest.EntitiesList
            }
            .Schedule(state.Dependency);
        }
        
        [BurstCompile]
        public struct ReadElementsFromCollection : IJobParallelForDefer
        {
            public NativeArray<Entity> EntitiesList;
            
            public void Execute(int index)
            {
                Debug.Log($"Is entity null? {EntitiesList[index] == Entity.Null}");
            }
        }

        [BurstCompile]
        public partial struct ClearStorageLists : IJob
        {
            public NativeList<Entity> EntitiesList;

            public void Execute()
            {
                EntitiesList.Clear();
            }
        }
    }

So this is not working - (InvalidOperationException: The previously scheduled job CollectionsTestingSystem1:WriteElementsToCollection writes to the Unity.Collections.NativeList1[Unity.Entities.Entity] WriteElementsToCollection.EntitiesList. You are trying to schedule a new job CollectionsTestingSystem2:ReadElementsFromCollection, which writes to the same Unity.Collections.NativeList1[Unity.Entities.Entity] (via ReadElementsFromCollection.EntitiesList). To guarantee safety, you must include CollectionsTestingSystem1:WriteElementsToCollection as a dependency of the newly scheduled job)

Question:
How should I rewrite the code to make this work? I guess I should use UnsafeList and pointer handling, but I am struggling to understand how. And again - I know I should ideally use DynamicBuffer for this type of work, but I am trying to understand a different subject, so DynamicBuffer is not an answer I am looking for.

Thanks!

EDIT: No UnsafeList or pointer usage needed, the problem is in my singleton usage, as pointed by @eizenhorn in the post below. SOLVED for now!

SystemAPI.GetSingleton write read only dependency for that type for both systems, which means jobs can run in parallel, which means your map can be accessed for write and read by different jobs at the same time. Which is race condition and this is why you see this error. You need explicitly tell in first system that singleton will be used for RW access. You can add var dependencyOnlyLookup = SystemAPI.GetComponentLookup<AnimationDataSingleton>(false); to your first system (writing to native container) just this row, doing nothing in your code (but adding proper write dependency), or write extension like that (internal access required) and use it in OnCreate to explicitly manipulate read/write fence. (or make entity query for that singleton in on create with WithAllRW but that’s a bit more boilerplate)

EDIT:
As discussed in discord, for your case GetSingletonRW also will be enough.

1 Like