Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Bug Burst compile error and IJobEntity dependency troubleshooting

Discussion in 'Entity Component System' started by Loofou, Aug 31, 2022.

  1. Loofou

    Loofou

    Joined:
    Aug 22, 2012
    Posts:
    25
    Hey everyone,

    I am working with ECS for quite a while now, so I am definitely not a beginner. Just yesterday I ran into a baffling burst compile error, that I just can't seem to track down, so I was hoping I can get some help here.

    The error message I am getting:
    Code (CSharp):
    1. (0,0): Burst error BC1091: External and internal calls are not allowed inside static constructors: Interop.BCrypt.BCryptGenRandom(System.IntPtr hAlgorithm, byte* pbBuffer, int cbBuffer, int dwFlags)
    2.  
    3. While compiling job:
    4. Unity.Entities.JobEntityBatchIndexExtensions+JobEntityBatchIndexProducer`1[[Tellas.Storylets.ECS.Systems.FindAvailablePersonsJob, Tellas.Storylets, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]], Unity.Entities, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null::Execute(Unity.Entities.JobEntityBatchIndexExtensions+JobEntityBatchIndexWrapper`1[[Tellas.Storylets.ECS.Systems.FindAvailablePersonsJob, Tellas.Storylets, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]&, Unity.Entities, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null|System.IntPtr, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|System.IntPtr, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|Unity.Jobs.LowLevel.Unsafe.JobRanges&, UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null|System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089)
    5. Unity.Entities.JobEntityBatchIndexExtensions+JobEntityBatchIndexProducer`1[[Tellas.Storylets.ECS.Systems.RemoveDuplicatesJob, Tellas.Storylets, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]], Unity.Entities, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null::Execute(Unity.Entities.JobEntityBatchIndexExtensions+JobEntityBatchIndexWrapper`1[[Tellas.Storylets.ECS.Systems.RemoveDuplicatesJob, Tellas.Storylets, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]&, Unity.Entities, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null|System.IntPtr, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|System.IntPtr, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089|Unity.Jobs.LowLevel.Unsafe.JobRanges&, UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null|System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089)
    6.  
    The system in which the error is supposedly happening:
    Code (CSharp):
    1. using Tellas.Character.ECS.Components;
    2. using Tellas.Core.ECS;
    3. using Tellas.PhysicsEngine.ECS.Components;
    4. using Tellas.Storylets.ECS.Components;
    5. using Unity.Burst;
    6. using Unity.Collections;
    7. using Unity.Entities;
    8. using Unity.Transforms;
    9.  
    10. namespace Tellas.Storylets.ECS.Systems
    11. {
    12.  
    13.     [UpdateInGroup(typeof(TellasLateSimulationSystemGroup))]
    14.     public partial class FindAvailablePersonsSystem : SystemBase
    15.     {
    16.         EndSimulationEntityCommandBufferSystem commandBufferSystem;
    17.  
    18.         protected override void OnCreate()
    19.         {
    20.             commandBufferSystem = World.GetExistingSystem<EndSimulationEntityCommandBufferSystem>();
    21.         }
    22.  
    23.         protected override void OnUpdate()
    24.         {
    25.             EntityCommandBuffer ecb = commandBufferSystem.CreateCommandBuffer();
    26.  
    27. //Can be parallelized
    28.             Entities
    29.                .WithName("RemoveAvailablePersonTag")
    30.                .WithAll<Tag_AvailablePerson>()
    31.                .WithStructuralChanges()
    32.                .ForEach((Entity e) =>
    33.                 {
    34.                     //
    35.                     EntityManager.RemoveComponent<Tag_AvailablePerson>(e);
    36.                 }).Run();
    37.  
    38. //not sure how to parallelize - remove and recreate efficient enough?
    39.             Entities
    40.                .WithName("ClearAvailableToTalk")
    41.                .WithAll<AvailableToTalk>()
    42.                .WithAll<AvailableToTalk>()
    43.                .WithStructuralChanges()
    44.                .ForEach((Entity e) =>
    45.                 {
    46.                     //
    47.                     EntityManager.GetBuffer<AvailableToTalk>(e).Clear();
    48.                 }).Run();
    49.  
    50.             Dependency = new FindAvailablePersonsJob
    51.                 {
    52.                     canTalks = GetComponentDataFromEntity<CanTalk>()
    53.                   , persons = GetComponentDataFromEntity<Person>()
    54.                   , availablePersons = GetComponentDataFromEntity<Tag_AvailablePerson>()
    55.                   , ecb = ecb.AsParallelWriter()
    56.                 }.ScheduleParallel(Dependency);
    57.  
    58.             Dependency = new RemoveDuplicatesJob
    59.                 {
    60.                     ecb = ecb.AsParallelWriter()
    61.                 }.ScheduleParallel(Dependency);
    62.  
    63.             commandBufferSystem.AddJobHandleForProducer(Dependency);
    64.         }
    65.     }
    66.  
    67.     [BurstCompile]
    68.     [WithAll(typeof(Tag_FindAvailablePersons))]
    69.     [WithAll(typeof(Tag_PhysicsTrigger))]
    70.     [WithAll(typeof(CollisionTriggerResult))]
    71.     public partial struct FindAvailablePersonsJob : IJobEntity
    72.     {
    73.         [ReadOnly] public ComponentDataFromEntity<CanTalk> canTalks;
    74.         [ReadOnly] public ComponentDataFromEntity<Person> persons;
    75.         [ReadOnly] public ComponentDataFromEntity<Tag_AvailablePerson> availablePersons;
    76.  
    77.         public EntityCommandBuffer.ParallelWriter ecb;
    78.  
    79.         public void Execute(Entity e, [EntityInQueryIndex] int entityInQueryIndex, in Parent parent, in DynamicBuffer<CollisionTriggerResult> collisionResults)
    80.         {
    81.             Entity parentEntity = parent.Value;
    82.  
    83.             for(int i = 0; i < collisionResults.Length; ++i)
    84.             {
    85.                 CollisionTriggerResult triggerResult = collisionResults[i];
    86.  
    87.                 Entity other = triggerResult.a == e ? triggerResult.b : triggerResult.a;
    88.                 if(!canTalks.HasComponent(other) || !persons.HasComponent(other))
    89.                     continue;
    90.  
    91.                 if(!availablePersons.HasComponent(other))
    92.                     ecb.AddComponent<Tag_AvailablePerson>(entityInQueryIndex, other);
    93.  
    94.                 AvailableToTalk element = new(other, persons[other]);
    95.  
    96.                 ecb.AppendToBuffer(entityInQueryIndex, parentEntity, element);
    97.             }
    98.         }
    99.     }
    100.  
    101.     [BurstCompile]
    102.     [WithAll(typeof(AvailableToTalk))]
    103.     public partial struct RemoveDuplicatesJob : IJobEntity
    104.     {
    105.         public EntityCommandBuffer.ParallelWriter ecb;
    106.  
    107.         public void Execute(Entity e, [EntityInQueryIndex] int entityInQueryIndex, ref DynamicBuffer<AvailableToTalk> buffer)
    108.         {
    109.             ecb.RemoveComponent<AvailableToTalk>(entityInQueryIndex, e);
    110.             DynamicBuffer<AvailableToTalk> newBuffer = ecb.AddBuffer<AvailableToTalk>(entityInQueryIndex, e);
    111.  
    112.             NativeParallelHashSet<AvailableToTalk> set = new(buffer.Length, Allocator.Temp);
    113.             for(int i = 0; i < buffer.Length; ++i)
    114.             {
    115.                 set.Add(buffer[i]);
    116.             }
    117.  
    118.             int j = 0;
    119.             newBuffer.Length = set.Count();
    120.             foreach(AvailableToTalk availableToTalk in set)
    121.             {
    122.                 newBuffer[j++] = availableToTalk;
    123.             }
    124.  
    125.             set.Dispose();
    126.         }
    127.     }
    128.  
    129. }
    130.  
    The only constructors being called are on the `AvailableToTalk` dynamic buffer element and the hashset for duplicate removal (if there is a faster way, please let me know!)

    AvailableToTalk looks like this:

    Code (CSharp):
    1. [InternalBufferCapacity(8)]
    2.     public struct AvailableToTalk : IBufferElementData, IEquatable<AvailableToTalk>
    3.     {
    4.         // Actual value each buffer element will store.
    5.         public Entity value;
    6.         public Person person;
    7.  
    8.         public AvailableToTalk(Entity value, Person person)
    9.         {
    10.             this.value = value;
    11.             this.person = person;
    12.         }
    13.  
    14.         public bool Equals(AvailableToTalk other)
    15.         {
    16.             return value.Equals(other.value) && person.Equals(other.person);
    17.         }
    18.  
    19.         public override bool Equals(object obj)
    20.         {
    21.             return obj is AvailableToTalk other && Equals(other);
    22.         }
    23.  
    24.         public override int GetHashCode()
    25.         {
    26.             return HashCode.Combine(value, person);
    27.         }
    28.  
    29.         public static bool operator ==(AvailableToTalk left, AvailableToTalk right)
    30.         {
    31.             return left.Equals(right);
    32.         }
    33.  
    34.         public static bool operator !=(AvailableToTalk left, AvailableToTalk right)
    35.         {
    36.             return !left.Equals(right);
    37.         }
    38.     }
    And Person is simply an IComponentData with a single int for an ID, nothing more.

    I really don't understand where the error is coming from. I assume it is in the codegen from IJobEntity, but even there I don't see anything specific.

    Before I moved the code to an IJobEntity from a Entities.ForEach it was working fine without errors, but it was also running without burst originally, so no clue if the problem was already occuring before or not.

    Any help is greatly appreciated!
    Loofou
     
  2. Anthiese

    Anthiese

    Joined:
    Oct 13, 2013
    Posts:
    72
    It’s probably the HashCode.Combine call in AvailableToTalk.GetHashCode (the hash set should rely on GetHashCode to function). The .NET HashCode struct as implemented by the BCL Unity uses probably does some magic stuff in its static constructor (considering that the current version on dotnet/runtime main does so), so you’d want to derive your own hashing for that struct, or just use an IDE function to make one.
     
    Last edited: Aug 31, 2022
    Loofou likes this.
  3. Loofou

    Loofou

    Joined:
    Aug 22, 2012
    Posts:
    25
    Hm right, let me try to combine manually via prime multiplication.

    Edit:

    Yeah that one should've been obvious. Damn you code-completion automation :D

    Thanks for the hint on that, the error is gone. Unfortunately I get another error now, but I hope I can figure this one out myself.

    Code (CSharp):
    1. The system Tellas.Storylets.ECS.Systems.FindAvailablePersonsSystem reads Tellas.PhysicsEngine.ECS.Components.CollisionTriggerResult via FindAvailablePersonsJob but that type was not assigned to the Dependency property. To ensure correct behavior of other systems, the job or a dependency must be assigned to the Dependency property before returning from the OnUpdate method.
    It's a bit weird because I am adding both jobs to the Dependency field...
     
  4. Loofou

    Loofou

    Joined:
    Aug 22, 2012
    Posts:
    25
    Yeah I can't figure this one out either. All jobs are added to the Dependency field, I even removed the main-thread entities.foreach temporarily and it still complains. The only way to get it to stop is to not use ECBs at all and run everything on the main thread, but I refuse to go down that route unless there is really no other option.

    Usually when this error occured I did some weird stuff with jobhandle combines that it could not detect properly, but in this case everything is super straight forward so I really don't get it :/

    After writing 50 systems without any big issues, this one is really getting on my nerves :D
     
  5. sheredom

    sheredom

    Unity Technologies

    Joined:
    Jul 15, 2019
    Posts:
    300
    It's the HashCode.Combine - it uses some managed static's within the implementation, which Burst cannot handle. We need to improve that error though (at least give you a callstack that caused it perhaps).
     
    Loofou likes this.
  6. Loofou

    Loofou

    Joined:
    Aug 22, 2012
    Posts:
    25
    Right, thanks for confirming. And yes, a callstack would've been extremely helpful :)
    Replacing the HashCode.Combine with a manual hashing worked. I think I will replace the auto-creation template from Rider with a more burst-friendly approach.

    Right now I ran into a follow-up error (see my last message), which I also never had like this before. I'm sure there is a similar simple solution, but from my knowledge I can't figure it out.
     
  7. sheredom

    sheredom

    Unity Technologies

    Joined:
    Jul 15, 2019
    Posts:
    300
    That looks like a job system thing or something like that, way outside my wheelhouse (they only let me out to talk to users about Burst :D ). I'll try ask around though!
     
    Loofou likes this.
  8. Loofou

    Loofou

    Joined:
    Aug 22, 2012
    Posts:
    25
    Haha, no problem at all :D much appreciated!
     
  9. kevinmv

    kevinmv

    Unity Technologies

    Joined:
    Nov 15, 2018
    Posts:
    50
    @Loofou Can you try making a local edit to Unity.Entities/SystemState.cs?


    Code (CSharp):
    1.         internal void LogSafetyErrors()
    2.         {
    3.             if (!JobsUtility.JobDebuggerEnabled)
    4.                 return;
    5.  
    6.             var depMgr = m_DependencyManager;
    7.             if (SystemDependencySafetyUtility.FindSystemSchedulingErrors(m_SystemID, ref m_JobDependencyForReadingSystems, ref m_JobDependencyForWritingSystems, depMgr, out var details))
    8.             {
    9.                 bool logged = false;
    10.                 LogSafetyDetails(details, ref logged);
    11.  
    12.                 if (!logged)
    13.                 {
    14.                     Debug.LogError("A system dependency error was detected but could not be logged accurately from Burst. Disable Burst compilation to see full error message.");
    15.                 }
    16.  
    17.                 m_DependencyManager->Safety.PanicSyncAll(); /// <--------Remove this line
    18.             }
    19.         }
    I know we have a bug fix yet to be released where safety errors were being suppressed making the "be sure to assign to Dependency" message appear instead of the real safety issue appearing. That could be the case here (only taking a quick glance at your code)
     
  10. Loofou

    Loofou

    Joined:
    Aug 22, 2012
    Posts:
    25
    Thanks for the answer! How can I make a local edit to this file without Unity redownloading the package every time I try to save?

    Edit:
    Nevermind, found it. (here, for anyone else looking: https://www.reddit.com/r/Unity3D/comments/hmbm46/can_i_edit_a_unity_package/)

    This does indeed change the error to

    Code (CSharp):
    1. [ERR] [UNITY] "InvalidOperationException: The previously scheduled job FindAvailablePersonsJob writes to the Unity.Entities.EntityCommandBuffer FindAvailablePersonsJob.JobData.ecb. You must call JobHandle.Complete() on the job FindAvailablePersonsJob, before you can write to the Unity.Entities.EntityCommandBuffer safely."
    So does that means I need one command buffer per job? I never knew that, always just reused the same one, but it makes sense. I hope the order is still dependent on the Dependency handle if I use two ecbs. I will try this.

    Edit2:

    I think I got it. My second job is reading the buffer I'm writing in the job before, but I'm not actually waiting for the ecb to be finished. I should probably find a better way to remove duplicates from my buffer :D

    Thanks everyone for the help! Much appreciated!
     
    Last edited: Aug 31, 2022
    kevinmv and sheredom like this.
  11. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,653
    upload_2022-9-2_10-32-50.png
    Your issue is
    ecb.AsParallelWriter
    In first job you call it and pass to job, and schedule, after that point any read\write access of ecb (safety checks inside
    AsParallelWriter
    check if someone write to ecb) - not allowed by safety system. What you should do in that case (if jobs depends on each other) is - declare
    ecbParallel
    right before first job and assign
    ecb.AsParallelWriter()
    , and just pass
    ecbParallel
    to both of your jobs.
     
    bb8_1 likes this.