Search Unity

Bug command Buffer sortKey not deterministic (simple test)

Discussion in 'Entity Component System' started by Elfinnik159, Mar 17, 2021.

  1. Elfinnik159

    Elfinnik159

    Joined:
    Feb 24, 2013
    Posts:
    145
    Hi,

    From the documentation, sortKey should provide determinism. However, this does not seem to be the case in all cases.
    Here's a simple example script:
    Code (CSharp):
    1. using Unity.Burst;
    2. using Unity.Collections;
    3. using Unity.Entities;
    4. using Unity.Jobs;
    5. using Unity.Mathematics;
    6. using Unity.Transforms;
    7. [AlwaysUpdateSystem]
    8. public class CommandBufferDetermineTestSystem : SystemBase
    9. {
    10.     BeginInitializationEntityCommandBufferSystem m_EntityCommandBufferSystem;
    11.     protected override void OnCreate()
    12.     {
    13.         base.OnCreate();
    14.         for (int i = 0; i < 9; i++)
    15.         {
    16.             var e = EntityManager.CreateEntity();
    17.             EntityManager.AddComponentData(e, new Translation { Value = 0 });
    18.             EntityManager.AddComponentData(e, new Rotation { Value = quaternion.identity });
    19.             if (i % 3 == 0)
    20.             {
    21.                 EntityManager.AddComponentData(e, new Spawner_1 { });
    22.             }
    23.             else if (i % 3 == 1)
    24.             {
    25.                 EntityManager.AddComponentData(e, new Spawner_2 { });
    26.             }
    27.             else
    28.             {
    29.                 EntityManager.AddComponentData(e, new Spawner_3 { });
    30.             }
    31.         }
    32.             m_EntityCommandBufferSystem = World.GetOrCreateSystem<BeginInitializationEntityCommandBufferSystem>();
    33.     }
    34.     int counter = 0;
    35.     protected override void OnUpdate()
    36.     {
    37.         counter++;
    38.         if (counter < 5) return;
    39.         var commandBuffer = m_EntityCommandBufferSystem.CreateCommandBuffer().AsParallelWriter();
    40.  
    41.        var sjh1= Entities.ForEach((Entity e,int entityInQueryIndex, ref Spawner_1 spawner) => {
    42.             spawner.ID = 1;
    43.             SpawnEntity(commandBuffer,entityInQueryIndex,1);
    44.             commandBuffer.DestroyEntity(entityInQueryIndex, e);
    45.         }).Schedule(Dependency);
    46.  
    47.         var sjh2 = Entities.ForEach((Entity e, int entityInQueryIndex, ref Spawner_2 spawner) => {
    48.             spawner.ID = 1;
    49.             SpawnEntity(commandBuffer, entityInQueryIndex,2);
    50.             commandBuffer.DestroyEntity(entityInQueryIndex, e);
    51.         }).Schedule(sjh1);
    52.         var sjh3 = Entities.ForEach((Entity e, int entityInQueryIndex, ref Spawner_3 spawner) => {
    53.             spawner.ID = 1;
    54.             SpawnEntity(commandBuffer, entityInQueryIndex, 3);
    55.             commandBuffer.DestroyEntity(entityInQueryIndex, e);
    56.         }).Schedule(sjh2);
    57.         Dependency = sjh3;
    58.         m_EntityCommandBufferSystem.AddJobHandleForProducer(Dependency);
    59.     }
    60.     public static void SpawnEntity(EntityCommandBuffer.ParallelWriter commandBuffer, int entityInQueryIndex,int ID)
    61.     {
    62.         UnityEngine.Debug.LogError($"Spawn entity with ID {ID} by sortKey = {entityInQueryIndex}");
    63.         var e = commandBuffer.CreateEntity(entityInQueryIndex);
    64.         commandBuffer.AddComponent<Translation>(entityInQueryIndex,e);
    65.         commandBuffer.AddComponent<Rotation>(entityInQueryIndex,e);
    66.         commandBuffer.AddComponent<SampleComponent_1>(entityInQueryIndex,e,new SampleComponent_1 { ID=ID});
    67.         commandBuffer.AddComponent<SampleComponent_2>(entityInQueryIndex,e);
    68.     }
    69. }
    70.  
    71. public struct Spawner_1 : IComponentData
    72. {
    73.     public int ID;
    74. }
    75. public struct Spawner_2 : IComponentData
    76. {
    77.     public int ID;
    78. }
    79. public struct Spawner_3 : IComponentData
    80. {
    81.     public int ID;
    82. }
    83. public struct SampleComponent_1 : IComponentData
    84. {
    85.     public int ID;
    86. }
    87. public struct SampleComponent_2 : IComponentData
    88. {
    89.     public float4x4 data;
    90. }
    I am watching SampleComponent_1.ID
    I expect one of two options:
    111222333
    123123123

    Spawn entity with ID 1 by sortKey = 0
    Spawn entity with ID 1 by sortKey = 1
    Spawn entity with ID 1 by sortKey = 2
    Spawn entity with ID 2 by sortKey = 0
    Spawn entity with ID 2 by sortKey = 1
    Spawn entity with ID 2 by sortKey = 2
    Spawn entity with ID 3 by sortKey = 0
    Spawn entity with ID 3 by sortKey = 1
    Spawn entity with ID 3 by sortKey = 2

    SampleComponent_1.ID values I get (I just select Entity in turn in EntityDebugger):
    Run #1) 231123312
    Run #2) 123312231
    Run #3) 231123312
    Run #4) 312231123

    As you can see from the test, commandBuffer can produce a completely random sequence.

    Obviously, this is due to the sequential start of tasks.
    However, I am running jobs sequentially and not in parallel. Therefore, the result is absolutely unexpected.
    Someone may say that this is logical, because commandBuffer does not remember the order of starting the job / recording time.
    However, if commandBuffer does the sorting by sortKey, then I should get this in the output:
    111222333

    If commandBuffer could remember the queue or start time of jobs or perform any sorting, it is obvious that the result would be like this:
    123123123

    Is this the expected result? That is, when we execute sequentially tasks and write to the buffer, we will get a completely random order of creation?

    Does this mean that I cannot use the commandBuffer in sequential jobs in this way and I have to (for example) manually change the sortKey (for example, every next Job will add +100000)?


    I have been looking for this problem in my large project for a very long time, because I was sure that it is deterministic. Or am I missing some part in the documentation? Is this the expected behavior?

    Entities 0.17.0 - p.41
    (I'm talking about several Jobs, in one Job SortKey is deterministic)
     
    Last edited: Mar 17, 2021
  2. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,270
    Using the same sortKey multiple times where commands could be recorded from different thread indices breaks determinism. Even though you are running the jobs serially, because you are scheduling the jobs and they are allowed to run on different threads (just not at the same time), then the jobs will have different thread indices.

    If commands are truly recorded serially, use a NativeReference as a counter for generating sortKey values.
     
    Elfinnik159 likes this.
  3. Elfinnik159

    Elfinnik159

    Joined:
    Feb 24, 2013
    Posts:
    145
    I changed the ID to EIQI, it made it easier to understand what you are talking about.
    Yes, it does sound logical and I fell into the trap again: I didn't see it in the documentation.
    https://docs.unity3d.com/Packages/com.unity.entities@0.17/manual/entity_command_buffer.html
    It says only about one Job, but it is not specified.

    In this case, it is a pity that we do not have an extension where, together with sortKey, we could pass the Job ID. It would also make it easier to understand that inter-Jobs determinism is not supported.