Search Unity

Feedback Writing Custom Job Types - Awesome Potential - Currently Too Difficult

Discussion in 'Entity Component System' started by PublicEnumE, Mar 31, 2020.

  1. PublicEnumE

    PublicEnumE

    Joined:
    Feb 3, 2019
    Posts:
    729
    I understand if this isn't a high priority at the moment, but here's a real world scenario that I really wish was better supported:

    I have several systems which need to schedule several jobs. Each of these jobs is 90% similar:
    • 3 of their component types are always the same, with the same read/write settings.
    • They all share a comment set of fields, including the same native collection types.
    • The logic in their Execute methods are almost exactly the same.
    Each job is also slightly unique:
    • In addition to the 3 common component types, each job may also work on a few unique component types.
    • Each job may need a few unique fields.
    • Some of the logic in their Execute methods will be unique.
    This scenario sounds like an excellent case for writing a custom job type! This would be a new job interface, which would:
    1. Let me write chunk-iteration job code once, which is then used by all such jobs (reducing duplicated code and user error).
    2. Hide that boilerplate code, exposing only a simple "Execute" method interface to the user.
    3. Limit the data which the user needs to specify when creating a new job, to the data which is actually unique to that job.
    So instead of writing this every time someone on my team wants to write one of these jobs:

    Code (CSharp):
    1. private struct Job : IJobForEachWithEntity_EBB<RunBehavior, RunBehaviorSchedule>
    2. {
    3.     // Boilerplate code to iterate through the behavior buffer.
    4.     // This code would be the same in every job.
    5.     // This is what I'd like to remove the need for, by creating a custom job type.
    6.     public int fooCount;
    7.  
    8.     [DeallocateOnJobCompletion]
    9.     public NativeArray<int> barNums;
    10.  
    11.     [NativeDisableContainerSafetyRestriction]
    12.     public BufferFromEntity<FooComponent> fooComponentBuffers;
    13.  
    14.     public void Execute(Entity entity, int index, DynamicBuffer<RunBehavior> runBehaviorBuffer, [ReadOnly] DynamicBuffer<RunBehaviorSchedule> runBehaviorScheduleBuffer)
    15.     {
    16.         DynamicBuffer<FooComponent> fooComponentBuffer = fooComponentBuffers[entity];
    17.  
    18.         for (int i = 0; i < runBehaviorScheduleBuffer.Length; i++)
    19.         {
    20.             RunBehaviorSchedule runBehaviorSchedule = runBehaviorScheduleBuffer[i];
    21.  
    22.             // We use another Buffer full of wrapped bools, to determine which behaviors should actually be executed.
    23.             if (!runBehaviorSchedule.ShouldExecuteBehaviorAtIndex())
    24.             {
    25.                 continue;
    26.             }
    27.  
    28.             RunBehavior runBehavior = runBehaviorBuffer[i];
    29.  
    30.             ExecuteBehavior(entity, ref runBehavior, fooCount, barNums, fooComponentBuffer);
    31.  
    32.             runBehaviorBuffer[i] = runBehavior;
    33.         }
    34.     }
    35.  
    36.     // Where the actual behavior is performed.
    37.     // This is the only code which would be unique in each behavior job.
    38.     private void ExecuteBehavior(Entity entity, ref RunBehavior runBehavior, int fooCount, NativeArray<int> barNums, DynamicBuffer<FooComponent> fooComponentBuffer)
    39.     {
    40.         // perform behavior
    41.     }
    42. }
    They could instead write this:

    Code (CSharp):
    1. private struct Job : IJobExecuteBehavior<RunBehavior>
    2. {
    3.     private void ExecuteBehavior(Entity entity, ref RunBehavior runBehavior, int fooCount, NativeArray<int> barNums, DynamicBuffer<FooComponent> fooComponentBuffer)
    4.     {
    5.         // perform behavior
    6.     }
    7. }
    This is just like how Unity's built in job types work. job interfaces like like IJobChunk or IJobForEach hide chunk iteration behind the scenes, so you only need to specify the data that's unique to the job you are writing.

    However, my attempts to write my own versions of jobs which have the same internal logic as IJobChunk or IJobForEach quickly ran into limitations. Many of the utility classes and job types are marked internal. Things like:
    ...are locked down pretty tight, to the point where we'd need to create our own clones of most basic DOTS types (like EntityQuery) to mimic the code used by IJobChunk and the like.

    The guidelines at the Custom Job Types page (https://docs.unity3d.com/Packages/com.unity.jobs@0.1/manual/custom_job_types.html) is awesome. It will get you a good deal of the way there. But it seems aimed more at custom job types which are fairly simple - nothing like the prefilterdata tricks that IJobChunk or IJobForEach is doing.

    The potential here is huge! Being able to wrap logic in our own custom job types would be of huge convenience to teams and power users. But right now it seems like this is not quite viable.

    Please consider opening up these classes in the future, or exposing new ways to make this kind of thing possible. Thank you, and please let me know if I am missing something right under my nose. :)

    Cheers!
     
    Last edited: Apr 1, 2020
  2. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    Is there a reason why you can't build those higher level job types on top of IJobChunk?
     
    PublicEnumE likes this.
  3. PublicEnumE

    PublicEnumE

    Joined:
    Feb 3, 2019
    Posts:
    729
    Burst support was the main reason I thought that wouldn’t work.

    I tried writing a set of generic wrapper jobs which implemented IJobChunk (similar to the IJobForEach ‘Process’ and ‘Infer’ wrapper jobs).

    When I would ‘schedule’ my job structs, they would get added as a field to the correct wrapper job type. I would create an entity query from the Read/Write attributes used in my “wrapped” struct’s Execute method. Then I would Schedule the wrapper job with the job system using that query.

    the problem was Burst support. If I BurstCompiled my wrapper jobs, then my “wrapped” jobs would then always be Bursted. Which is good until I need to set breakpoints and debug one of them. Then I would have to turn a burst off at the wrapper job level, which would turn it off everywhere, potentially bringing things to a standstill.

    But If I added a BurstCompile attribute to just my “wrapped” job struct, and then added it as a field to my non-Burst wrapper job, would it actually be BurstCompiled? I was unclear about that. I thought only actual job types could make use of the BurstCompileAttribute.

    if any of my language is unclear, please let me know. I’ll post some code samples when I’m able.
     
    Last edited: Apr 2, 2020
  4. PublicEnumE

    PublicEnumE

    Joined:
    Feb 3, 2019
    Posts:
    729
    Alright! Here are some code examples of the two scenarios described in the previous post:

    Scenario #1: [BurstCompile] goes on the WrapperJob:

    Code (CSharp):
    1. public interface IJobCustom<T> where T : struct, IBufferElementData
    2. {
    3.     void Execute(Entity entity, ref T t0, ref Baz baz, NativeArray<Foo> foos);
    4. }
    5.  
    6.  
    7. // In Scenario #1, the BurstCompileAttribute goes on the WrapperJob. Doesn't this mean the "Wrapped" job will always be Burst Compiled...
    8. // ...(meaning an individual IJobCustom<> can't be debugged without removing this attribute at the WrapperJob<> level)?
    9. [BurstCompile]
    10. public struct WrapperJob<TJobCustom, T0> : IJobChunk where TJobCustom : struct, IJobCustom<T0> where T0 : struct, IBufferElementData
    11. {
    12.     // wrapped job ends up here
    13.     public TJobCustom wrappedJob;
    14.  
    15.     [ReadOnly]
    16.     [DeallocateOnJobCompletion]
    17.     public NativeArray<Foo> foos;
    18.  
    19.     [ReadOnly]
    20.     public ArchetypeChunkEntityType entityType;
    21.     public ArchetypeChunkComponentType<Baz> bazType;
    22.     public ArchetypeChunkBufferType<T0> t0Type;
    23.     public bool isReadOnly_T0;
    24.  
    25.     public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    26.     {
    27.         var entities = chunk.GetNativeArray(entityType);
    28.         var bazArray = chunk.GetNativeArray(bazType);
    29.         var t0Buffers = chunk.GetBufferAccessor(t0Type);
    30.  
    31.         for (int i = 0; i < chunk.Count; i++)
    32.         {
    33.             var entity = entities[i];
    34.             var baz = bazArray[i];
    35.             var t0Buffer = t0Buffers[i];
    36.  
    37.             for (int iT0 = 0; iT0 < t0Buffer.Length; iT0++)
    38.             {
    39.                 var t0 = t0Buffer[iT0];
    40.  
    41.                 // wrapped job Execute method is called
    42.                 wrappedJob.Execute(entity, ref t0, ref baz, foos);
    43.  
    44.                 bazArray[i] = baz;
    45.  
    46.                 if(isReadOnly_T0)
    47.                 {
    48.                     t0Buffer[iT0] = t0;
    49.                 }
    50.             }
    51.         }
    52.     }
    53. }
    54.  
    55. public struct WrappedCustomJob : IJobCustom<Bar>
    56. {
    57.     public void Execute(Entity entity, ref Bar t0, ref Baz baz, NativeArray<Foo> foos)
    58.     {
    59.         // do job-specific logic here.
    60.     }
    61. }
    62.  
    Scenario #2: [BurstCompile] goes on each "Wrapped" job struct:

    Code (CSharp):
    1. public interface IJobCustom<T> where T : struct, IBufferElementData
    2. {
    3.     void Execute(Entity entity, ref T t0, ref Baz baz, NativeArray<Foo> foos);
    4. }
    5.  
    6. public struct WrapperJob<TJobCustom, T0> : IJobChunk where TJobCustom : struct, IJobCustom<T0> where T0 : struct, IBufferElementData
    7. {
    8.  
    9. // wrapped job ends up here
    10.     public TJobCustom wrappedJob;
    11.  
    12.     [ReadOnly]
    13.     [DeallocateOnJobCompletion]
    14.     public NativeArray<Foo> foos;
    15.  
    16.     [ReadOnly]
    17.     public ArchetypeChunkEntityType entityType;
    18.     public ArchetypeChunkComponentType<Baz> bazType;
    19.     public ArchetypeChunkBufferType<T0> t0Type;
    20.     public bool isReadOnly_T0;
    21.  
    22.     public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    23.     {
    24.         var entities = chunk.GetNativeArray(entityType);
    25.         var bazArray = chunk.GetNativeArray(bazType);
    26.         var t0Buffers = chunk.GetBufferAccessor(t0Type);
    27.  
    28.         for (int i = 0; i < chunk.Count; i++)
    29.         {
    30.             var entity = entities[i];
    31.             var baz = bazArray[i];
    32.             var t0Buffer = t0Buffers[i];
    33.  
    34.             for (int iT0 = 0; iT0 < t0Buffer.Length; iT0++)
    35.             {
    36.                 var t0 = t0Buffer[iT0];
    37.  
    38.                 // wrapped job Execute method is called
    39.                 wrappedJob.Execute(entity, ref t0, ref baz, foos);
    40.  
    41.                 bazArray[i] = baz;
    42.  
    43.                 if(isReadOnly_T0)
    44.                 {
    45.                     t0Buffer[iT0] = t0;
    46.                 }
    47.             }
    48.         }
    49.     }
    50. }
    51.  
    52. // In Scenario #2, the BurstCompileAttribute goes on the "Wrapped" job struct. Will this even be caught by the Burst Compiler...
    53. // ...since it's not an actual job type? Will this still work if WrapperJob<> isn't Burst compiled? Won't I miss out on...
    54. // ...performance if WrapperJob isn't Burst compiled?
    55. [BurstCompile]
    56. public struct WrappedCustomJob : IJobCustom<Bar>
    57. {
    58.     public void Execute(Entity entity, ref Bar t0, ref Baz baz, NativeArray<Foo> foos)
    59.     {
    60.         // do job-specific logic here.
    61.     }
    62. }
    63.  
    Granted, all of this generic Bursting seems like it would only be possible with 1.3.0-preview.8 or later, which doesn't seem to be compatible with the latest entities package.

    Right now, the Burst inspector doesn't show either of those Scenarios Bursting any of these jobs (I'm using Entities 0.8.0-preview.8).

    Thanks for any and all advice! :)
     
    Last edited: Apr 1, 2020
  5. PublicEnumE

    PublicEnumE

    Joined:
    Feb 3, 2019
    Posts:
    729
    Also worth noting that in either of these scenarios, I would need to make a custom System class, with a custom, public GetEntityQuery() method, since ComponentSystemBase.GetEntityQuery() is protected.


    Code (CSharp):
    1. public abstract class CustomSystemBase : SystemBase
    2. {
    3.     public EntityQuery GetEntityQueryPublic(ComponentType[] componentTypes)
    4.     {
    5.         return GetEntityQuery(componentTypes);
    6.     }
    7. }
    8.  
    9. public static class IJobCustomExtensions
    10. {
    11.     public static JobHandle Schedule<TJob, T0>(this TJob jobData, CustomSystemBase system, JobHandle dependentOn) where TJob : struct, IJobCustom<T0> where T0 : struct, IBufferElementData
    12.     {
    13.         // worth noting that this array will contain more than just the T0 type. It will also contain...
    14.         // the ComponentType for the 'Baz' Component, which will be part of each of these jobs.
    15.         ComponentType[] componentTypes = GetIJobCustomComponentTypes(jobData.GetType());
    16.  
    17.         // normally I would cache this afer creating it the first time.
    18.         EntityQuery query = system.GetEntityQueryPublic(componentTypes);
    19.  
    20.         WrapperJob<TJob, T0> wrapperJob = new WrapperJob<TJob, T0>
    21.         {
    22.             /*
    23.             construct wrapper job data here
    24.             */
    25.  
    26.             wrappedJob = jobData
    27.         };
    28.  
    29.         return wrapperJob.ScheduleParallel(query, dependentOn);
    30.     }
    31. }
    32.  
    33. public class MySystem : CustomSystemBase
    34. {
    35.     protected override void OnUpdate()
    36.     {
    37.         new WrappedCustomJob
    38.         {
    39.         }.Schedule<WrappedCustomJob, Bar>(this, Dependency);
    40.     }
    41.  
    42.     public struct WrappedCustomJob : IJobCustom<Bar>
    43.     {
    44.         public void Execute(Entity entity, ref Bar t0, ref Baz baz, NativeArray<Foo> foos)
    45.         {
    46.             // do job-specific logic here.
    47.         }
    48.     }
    49. }
    This is mimicking the internal
    ComponentSystemBase.GetEntityQueryInternal()
    methods that you guys call when building queries for IJobForEach jobs.
     
    Last edited: Apr 1, 2020
  6. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,685
    About how you can debug directly inside bursted codepath (only for native debuggers)
     
    PublicEnumE likes this.
  7. PublicEnumE

    PublicEnumE

    Joined:
    Feb 3, 2019
    Posts:
    729
    Hooray! Thank you, I did not know about this (and such soothing narration).

    However, none of my jobs are currently being Burst compiled. I've been digging into this. I seems that calling an extension to schedule a generic job causes the job not to be Burst Compiled.

    Here's my code(below). When I run this in the editor, Sample.WrapperJob<,> does not show up in the Burst Inspector.

    However, if I schedule a WrapperJob<,> directly from inside my System update function, it will be BurstCompiled.

    (Scheduling Extension Method is at line #89)
    (My System calls this extension method at line #174)

    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using Unity.Burst;
    4. using Unity.Jobs;
    5. using Unity.Collections;
    6. using Unity.Entities;
    7.  
    8. namespace Sample
    9. {
    10.     public struct Foo
    11.     {
    12.         public int num;
    13.     }
    14.  
    15.     public struct Bar : IBufferElementData
    16.     {
    17.         public int num;
    18.     }
    19.  
    20.     public struct Baz : IComponentData
    21.     {
    22.         public int num;
    23.     }
    24.  
    25.     public interface IJobCustom<T> where T : struct, IBufferElementData
    26.     {
    27.         void Execute(Entity entity, ref T t0, ref Baz baz, NativeArray<Foo> foos);
    28.     }
    29.  
    30.     [BurstCompile]
    31.     public struct WrapperJob<TJob, T0> : IJobChunk where TJob : struct, IJobCustom<T0> where T0 : struct, IBufferElementData
    32.     {
    33.         // wrapped job ends up here
    34.         public TJob wrappedJob;
    35.  
    36.         [ReadOnly]
    37.         [DeallocateOnJobCompletion]
    38.         public NativeArray<Foo> foos;
    39.  
    40.         [ReadOnly]
    41.         public ArchetypeChunkEntityType entityType;
    42.         public ArchetypeChunkComponentType<Baz> bazType;
    43.         public ArchetypeChunkBufferType<T0> t0Type;
    44.         public bool isReadOnly_T0;
    45.  
    46.         public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    47.         {
    48.             var entities = chunk.GetNativeArray(entityType);
    49.             var bazArray = chunk.GetNativeArray(bazType);
    50.             var t0Buffers = chunk.GetBufferAccessor(t0Type);
    51.  
    52.             for (int i = 0; i < chunk.Count; i++)
    53.             {
    54.                 var entity = entities[i];
    55.                 var baz = bazArray[i];
    56.                 var t0Buffer = t0Buffers[i];
    57.  
    58.                 for (int iT0 = 0; iT0 < t0Buffer.Length; iT0++)
    59.                 {
    60.                     var t0 = t0Buffer[iT0];
    61.  
    62.                     // wrapped job Execute method is called
    63.                     wrappedJob.Execute(entity, ref t0, ref baz, foos);
    64.  
    65.                     bazArray[i] = baz;
    66.  
    67.                     if (!isReadOnly_T0)
    68.                     {
    69.                         t0Buffer[iT0] = t0;
    70.                     }
    71.                 }
    72.             }
    73.         }
    74.     }
    75.  
    76.  
    77.     public abstract class CustomSystemBase : SystemBase
    78.     {
    79.         public EntityQuery GetEntityQueryPublic(ComponentType[] componentTypes)
    80.         {
    81.             return GetEntityQuery(componentTypes);
    82.         }
    83.     }
    84.  
    85.     public static class IJobCustomExtensions
    86.     {
    87.         private static Dictionary<Type, ComponentType[]> componentTypesByJobType = new Dictionary<Type, ComponentType[]>();
    88.  
    89.         public static JobHandle Schedule<TJob, T0>(this TJob jobData, CustomSystemBase system, JobHandle dependentOn) where TJob : struct, IJobCustom<T0> where T0 : struct, IBufferElementData
    90.         {
    91.             EntityQuery query = GetExistingEntityQuery<TJob>(system);
    92.  
    93.             if (query == null)
    94.             {
    95.                 ComponentType[] componentTypes;
    96.                 if (!componentTypesByJobType.TryGetValue(typeof(TJob), out componentTypes))
    97.                 {
    98.                     componentTypes = GetIJobCustomComponentTypes<T0>();
    99.                 }
    100.  
    101.                 query = system.GetEntityQueryPublic(componentTypes);
    102.  
    103.                 system.RequireForUpdate(query);
    104.  
    105.                 if (query.CalculateChunkCount() == 0)
    106.                 {
    107.                     return dependentOn;
    108.                 }
    109.             }
    110.  
    111.             WrapperJob<TJob, T0> wrapperJob = new WrapperJob<TJob, T0>
    112.             {
    113.                 wrappedJob = jobData,
    114.                 foos = new NativeArray<Foo>(10, Allocator.TempJob),
    115.                 entityType = system.EntityManager.GetArchetypeChunkEntityType(),
    116.                 bazType = system.EntityManager.GetArchetypeChunkComponentType<Baz>(false),
    117.                 t0Type = system.EntityManager.GetArchetypeChunkBufferType<T0>(false),
    118.                 isReadOnly_T0 = false
    119.             };
    120.  
    121.             return wrapperJob.ScheduleParallel(query, dependentOn);
    122.         }
    123.  
    124.         private static EntityQuery GetExistingEntityQuery<TJob>(ComponentSystemBase system) where TJob : struct
    125.         {
    126.             ComponentType[] componentTypes;
    127.             if (!componentTypesByJobType.TryGetValue(typeof(TJob), out componentTypes))
    128.             {
    129.                 return null;
    130.             }
    131.  
    132.             for (var i = 0; i != system.EntityQueries.Length; i++)
    133.             {
    134.                 if (system.EntityQueries[i].CompareComponents(componentTypes))
    135.                     return system.EntityQueries[i];
    136.             }
    137.  
    138.             return null;
    139.         }
    140.  
    141.         private static ComponentType[] GetIJobCustomComponentTypes<T0>() where T0 : struct, IBufferElementData
    142.         {
    143.             // Temporary. A final version would use reflection to find the read/write attributes from the wrapper job's Execute method.
    144.             return new ComponentType[]
    145.                 {
    146.                 ComponentType.ReadWrite<T0>(),
    147.                 ComponentType.ReadWrite<Baz>()
    148.                 };
    149.         }
    150.     }
    151.  
    152.     public class MySystem : CustomSystemBase
    153.     {
    154.         protected override void OnCreate()
    155.         {
    156.             Entity entity = EntityManager.CreateEntity(new ComponentType(typeof(Bar)), new ComponentType(typeof(Baz)));
    157.  
    158.             DynamicBuffer<Bar> barBuffer = EntityManager.GetBuffer<Bar>(entity);
    159.  
    160.             Bar bar = new Bar
    161.             {
    162.                 num = 10
    163.             };
    164.  
    165.             barBuffer.Add(bar);
    166.  
    167.         }
    168.  
    169.         protected override void OnUpdate()
    170.         {
    171.             Dependency = new WrappedCustomJob
    172.             {
    173.                 toAdd = 10
    174.             }.Schedule<WrappedCustomJob, Bar>(this, Dependency);
    175.         }
    176.  
    177.         public struct WrappedCustomJob : IJobCustom<Bar>
    178.         {
    179.             public int toAdd;
    180.  
    181.             public void Execute(Entity entity, ref Bar t0, ref Baz baz, NativeArray<Foo> foos)
    182.             {
    183.                 // do custom logic here
    184.                 t0.num += toAdd;
    185.             }
    186.         }
    187.     }
    188. }
    Is this the issue that was addressed in Burst 1.3.0.-preview.8, and described in the release notes, below? :
    If so, any idea when we might be able to use Burst 1.3.0.-preview.8 with the Entities package?

    Many thanks! :)
     
    Last edited: Apr 1, 2020
  8. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,685
    Burst 1.3.0.-preview.9 already exist in package manager
     
  9. PublicEnumE

    PublicEnumE

    Joined:
    Feb 3, 2019
    Posts:
    729
    I ran into errors when trying to use it with the current Entities package. Have you had success using them together?
     
  10. PublicEnumE

    PublicEnumE

    Joined:
    Feb 3, 2019
    Posts:
    729
    Ok, an editor restart resolved those errors.

    But the issue remains. With the latest Burst package, scheduling a generic job from a generic extension method will still cause it to not be Burst compiled.
     
  11. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    Sadly it's not the same issue and is currently not a supported use case. They wrote documentation on what is and isn't supported.

    https://docs.unity3d.com/Packages/com.unity.burst@1.3/manual/index.html#generic-jobs

    -edit-

    side note more on topic. I love custom jobs, it's often what I get the most performance boost from and I've written a dozen or so.
     
    PublicEnumE likes this.
  12. PublicEnumE

    PublicEnumE

    Joined:
    Feb 3, 2019
    Posts:
    729
    @Joachim_Ante, Looks like building it on top of IJobChunk has hit a roadblock here. Or, I'm missing something. Is there another way to approach this?
     
    Last edited: Apr 2, 2020
  13. PublicEnumE

    PublicEnumE

    Joined:
    Feb 3, 2019
    Posts:
    729
    I was able to get the Wrapper Job to Burst-compile, by embedding it inside of a generic System:
    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using Unity.Burst;
    4. using Unity.Jobs;
    5. using Unity.Collections;
    6. using Unity.Entities;
    7.  
    8. namespace Sample
    9. {
    10.     public struct Foo
    11.     {
    12.         public int num;
    13.     }
    14.  
    15.     public struct Bar : IBufferElementData
    16.     {
    17.         public int num;
    18.     }
    19.  
    20.     public struct Baz : IComponentData
    21.     {
    22.         public int num;
    23.     }
    24.  
    25.     public interface IJobCustom<T> where T : struct, IBufferElementData
    26.     {
    27.         void Execute(Entity entity, ref T t0, ref Baz baz, NativeArray<Foo> foos);
    28.     }
    29.  
    30.     public abstract class CustomSystemBase : SystemBase
    31.     {
    32.         protected static Dictionary<Type, ComponentType[]> componentTypesByJobType = new Dictionary<Type, ComponentType[]>();
    33.  
    34.         protected static EntityQuery GetExistingEntityQuery<TJob>(ComponentSystemBase system) where TJob : struct
    35.         {
    36.             ComponentType[] componentTypes;
    37.             if (!componentTypesByJobType.TryGetValue(typeof(TJob), out componentTypes))
    38.             {
    39.                 return null;
    40.             }
    41.  
    42.             for (var i = 0; i != system.EntityQueries.Length; i++)
    43.             {
    44.                 if (system.EntityQueries[i].CompareComponents(componentTypes))
    45.                     return system.EntityQueries[i];
    46.             }
    47.  
    48.             return null;
    49.         }
    50.  
    51.         protected static ComponentType[] GetIJobCustomComponentTypes<T0>() where T0 : struct, IBufferElementData
    52.         {
    53.             // Temporary. A final version would use reflection to find the read/write attributes from the wrapper job's Execute method.
    54.             return new ComponentType[]
    55.                 {
    56.                 ComponentType.ReadWrite<T0>(),
    57.                 ComponentType.ReadWrite<Baz>()
    58.                 };
    59.         }
    60.     }
    61.  
    62.     public abstract class CustomSystemBase<TJob, T0> : CustomSystemBase where TJob : struct, IJobCustom<T0> where T0 : struct, IBufferElementData
    63.     {
    64.         [BurstCompile]
    65.         public struct WrapperJob : IJobChunk
    66.         {
    67.             // wrapped job ends up here
    68.             public TJob wrappedJob;
    69.  
    70.  
    71.             [ReadOnly]
    72.             [DeallocateOnJobCompletion]
    73.             public NativeArray<Foo> foos;
    74.  
    75.             [ReadOnly]
    76.             public ArchetypeChunkEntityType entityType;
    77.             public ArchetypeChunkComponentType<Baz> bazType;
    78.             public ArchetypeChunkBufferType<T0> t0Type;
    79.             public bool isReadOnly_T0;
    80.  
    81.             public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    82.             {
    83.                 var entities = chunk.GetNativeArray(entityType);
    84.                 var bazArray = chunk.GetNativeArray(bazType);
    85.                 var t0Buffers = chunk.GetBufferAccessor(t0Type);
    86.  
    87.                 for (int i = 0; i < chunk.Count; i++)
    88.                 {
    89.                     var entity = entities[i];
    90.                     var baz = bazArray[i];
    91.                     var t0Buffer = t0Buffers[i];
    92.  
    93.                     for (int iT0 = 0; iT0 < t0Buffer.Length; iT0++)
    94.                     {
    95.                         var t0 = t0Buffer[iT0];
    96.  
    97.                         // wrapped job Execute method is called
    98.                         wrappedJob.Execute(entity, ref t0, ref baz, foos);
    99.  
    100.                         bazArray[i] = baz;
    101.  
    102.                         if (!isReadOnly_T0)
    103.                         {
    104.                             t0Buffer[iT0] = t0;
    105.                         }
    106.                     }
    107.                 }
    108.             }
    109.         }
    110.  
    111.         protected JobHandle Schedule(TJob jobData)
    112.         {
    113.             EntityQuery query = GetExistingEntityQuery<TJob>(this);
    114.  
    115.             if (query == null)
    116.             {
    117.                 ComponentType[] componentTypes;
    118.                 if (!componentTypesByJobType.TryGetValue(typeof(TJob), out componentTypes))
    119.                 {
    120.                     componentTypes = GetIJobCustomComponentTypes<T0>();
    121.                 }
    122.  
    123.                 query = GetEntityQuery(componentTypes);
    124.  
    125.                 RequireForUpdate(query);
    126.  
    127.                 if (query.CalculateChunkCount() == 0)
    128.                 {
    129.                     return Dependency;
    130.                 }
    131.             }
    132.  
    133.             WrapperJob wrapperJob = new WrapperJob
    134.             {
    135.                 wrappedJob = jobData,
    136.                 foos = new NativeArray<Foo>(10, Allocator.TempJob),
    137.                 entityType = EntityManager.GetArchetypeChunkEntityType(),
    138.                 bazType = EntityManager.GetArchetypeChunkComponentType<Baz>(false),
    139.                 t0Type = EntityManager.GetArchetypeChunkBufferType<T0>(false),
    140.                 isReadOnly_T0 = false
    141.             };
    142.  
    143.             Dependency = wrapperJob.ScheduleParallel(query, Dependency);
    144.  
    145.             return Dependency;
    146.         }
    147.     }
    148.  
    149.     public class MySystem : CustomSystemBase<WrappedCustomJob, Bar>
    150.     {
    151.         protected override void OnUpdate()
    152.         {
    153.             Schedule(new WrappedCustomJob
    154.             {
    155.                 toAdd = 10
    156.             });
    157.         }
    158.     }
    159.  
    160.     public struct WrappedCustomJob : IJobCustom<Bar>
    161.     {
    162.         public int toAdd;
    163.  
    164.         public void Execute(Entity entity, ref Bar t0, ref Baz baz, NativeArray<Foo> foos)
    165.         {
    166.             // do custom logic here
    167.             t0.num += toAdd;
    168.         }
    169.     }
    170. }
    This will function. But it's a bit fragile, and certainly a bit awkward. For example: In this arrangement, it's now more convenient to define the "Wrapped" Job outside of the system that schedules it. Also, this trick will only work for scheduling that single job type from inside of the system.

    @Joachim_Ante, Thank you for commenting before, and It's not my intention to bug you. It would be helpful to have a comment from someone at Unity in response to the rest of this thread - Not to promise anything. But to at least know we're not missing a solution that already exists. :) Thank you to the whole team for your excellent guidance.
     
  14. PublicEnumE

    PublicEnumE

    Joined:
    Feb 3, 2019
    Posts:
    729
    Idle thought: I wonder if C# 8.0 default interface methods might allow us to write something like this:

    Code (CSharp):
    1. public class MySystem : SystemBase, IWrapperJobScheduler<WrappedCustomJob, Bar>
    2. {
    3.     protected override void OnUpdate()
    4.     {
    5.         this.Schedule(new WrappedCustomJob
    6.         {
    7.             toAdd = 10
    8.         });
    9.     }
    10. }
    11.  
    12. public struct WrappedCustomJob : IJobCustom<Bar>
    13. {
    14.     public int toAdd;
    15.     public void Execute(Entity entity, ref Bar t0, ref Baz baz, NativeArray<Foo> foos)
    16.     {
    17.         // do custom logic here
    18.         t0.num += toAdd;
    19.     }
    20. }
    That would solve the "can only schedule one job type" limitation from my previous post.

    I don't know what Burst might do with that. Fun to think about, but still, not a very polished solution. :)
     
    Last edited: Apr 3, 2020
  15. PublicEnumE

    PublicEnumE

    Joined:
    Feb 3, 2019
    Posts:
    729
    You can also get the Wrapper Job to Burst-compile by wrapping it in a genetic static class, and then referencing the static class from a non-generic system:
    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using Unity.Burst;
    4. using Unity.Jobs;
    5. using Unity.Collections;
    6. using Unity.Entities;
    7.  
    8. namespace Sample
    9. {
    10.     public struct Foo
    11.     {
    12.         public int num;
    13.     }
    14.  
    15.     public struct Bar : IBufferElementData
    16.     {
    17.         public int num;
    18.     }
    19.  
    20.     public struct Baz : IComponentData
    21.     {
    22.         public int num;
    23.     }
    24.  
    25.     public interface IJobCustom<T> where T : struct, IBufferElementData
    26.     {
    27.         void Execute(Entity entity, ref T t0, ref Baz baz, NativeArray<Foo> foos);
    28.     }
    29.  
    30.     public static class IJobCustomUtility<TJob, T0> where TJob : struct, IJobCustom<T0> where T0 : struct, IBufferElementData
    31.     {
    32.         [BurstCompile]
    33.         public struct WrapperJob : IJobChunk
    34.         {
    35.             // wrapped job ends up here
    36.             public TJob wrappedJob;
    37.  
    38.             [ReadOnly]
    39.             [DeallocateOnJobCompletion]
    40.             public NativeArray<Foo> foos;
    41.  
    42.             [ReadOnly]
    43.             public ArchetypeChunkEntityType entityType;
    44.             public ArchetypeChunkComponentType<Baz> bazType;
    45.             public ArchetypeChunkBufferType<T0> t0Type;
    46.             public bool isReadOnly_T0;
    47.  
    48.             public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
    49.             {
    50.                 var entities = chunk.GetNativeArray(entityType);
    51.                 var bazArray = chunk.GetNativeArray(bazType);
    52.                 var t0Buffers = chunk.GetBufferAccessor(t0Type);
    53.  
    54.                 for (int i = 0; i < chunk.Count; i++)
    55.                 {
    56.                     var entity = entities[i];
    57.                     var baz = bazArray[i];
    58.                     var t0Buffer = t0Buffers[i];
    59.  
    60.                     for (int iT0 = 0; iT0 < t0Buffer.Length; iT0++)
    61.                     {
    62.                         var t0 = t0Buffer[iT0];
    63.  
    64.                         // wrapped job Execute method is called
    65.                         wrappedJob.Execute(entity, ref t0, ref baz, foos);
    66.  
    67.                         bazArray[i] = baz;
    68.  
    69.                         if (!isReadOnly_T0)
    70.                         {
    71.                             t0Buffer[iT0] = t0;
    72.                         }
    73.                     }
    74.                 }
    75.             }
    76.         }
    77.  
    78.         public static JobHandle Schedule(TJob jobData, CustomSystemBase system, JobHandle dependentOn)
    79.         {
    80.             EntityQuery query = GetExistingEntityQuery(system);
    81.  
    82.             if (query == null)
    83.             {
    84.                 ComponentType[] componentTypes;
    85.                 if (!componentTypesByJobType.TryGetValue(typeof(TJob), out componentTypes))
    86.                 {
    87.                     componentTypes = GetIJobCustomComponentTypes();
    88.                 }
    89.  
    90.                 query = system.GetEntityQueryPublic(componentTypes);
    91.  
    92.                 system.RequireForUpdate(query);
    93.  
    94.                 if (query.CalculateChunkCount() == 0)
    95.                 {
    96.                     return dependentOn;
    97.                 }
    98.             }
    99.  
    100.             WrapperJob wrapperJob = new WrapperJob
    101.             {
    102.                 wrappedJob = jobData,
    103.                 foos = new NativeArray<Foo>(10, Allocator.TempJob),
    104.                 entityType = system.EntityManager.GetArchetypeChunkEntityType(),
    105.                 bazType = system.EntityManager.GetArchetypeChunkComponentType<Baz>(false),
    106.                 t0Type = system.EntityManager.GetArchetypeChunkBufferType<T0>(false),
    107.                 isReadOnly_T0 = false
    108.             };
    109.  
    110.             return wrapperJob.ScheduleParallel(query, dependentOn);
    111.         }
    112.  
    113.         private static Dictionary<Type, ComponentType[]> componentTypesByJobType = new Dictionary<Type, ComponentType[]>();
    114.  
    115.         private static EntityQuery GetExistingEntityQuery(ComponentSystemBase system)
    116.         {
    117.             ComponentType[] componentTypes;
    118.             if (!componentTypesByJobType.TryGetValue(typeof(TJob), out componentTypes))
    119.             {
    120.                 return null;
    121.             }
    122.  
    123.             for (var i = 0; i != system.EntityQueries.Length; i++)
    124.             {
    125.                 if (system.EntityQueries[i].CompareComponents(componentTypes))
    126.                     return system.EntityQueries[i];
    127.             }
    128.  
    129.             return null;
    130.         }
    131.  
    132.         private static ComponentType[] GetIJobCustomComponentTypes()
    133.         {
    134.             // Temporary. A final version would use reflection to find the read/write attributes from the wrapper job's Execute method.
    135.             return new ComponentType[]
    136.                 {
    137.                 ComponentType.ReadWrite<T0>(),
    138.                 ComponentType.ReadWrite<Baz>()
    139.                 };
    140.         }
    141.  
    142.     }
    143.  
    144.     public abstract class CustomSystemBase : SystemBase
    145.     {
    146.         public EntityQuery GetEntityQueryPublic(ComponentType[] componentTypes)
    147.         {
    148.             return GetEntityQuery(componentTypes);
    149.         }
    150.     }
    151.  
    152.     public class MySystem : CustomSystemBase
    153.     {
    154.         protected override void OnUpdate()
    155.         {
    156.             Dependency = IJobCustomUtility<WrappedCustomJob, Bar>.Schedule(new WrappedCustomJob
    157.             {
    158.                 toAdd = 10
    159.             }, this, Dependency);
    160.         }
    161.  
    162.         public struct WrappedCustomJob : IJobCustom<Bar>
    163.         {
    164.             public int toAdd;
    165.  
    166.             public void Execute(Entity entity, ref Bar t0, ref Baz baz, NativeArray<Foo> foos)
    167.             {
    168.                 // do custom logic here
    169.                 t0.num += toAdd;
    170.             }
    171.         }
    172.     }
    173. }
    This is probably the cleanest solution I've come up with. It gets rid of both earlier limitations (1. You can define the wrapped job inside the system class, and 2. You can call any number of job types from that system).

    Note: You would still need to define a custom system base class, though, to create a public wrapper for ComponentSystemBase.GetEntityQuery().

    EntityManager.CreateEntityQuery() is not the same.
     
    Last edited: Apr 3, 2020
  16. PublicEnumE

    PublicEnumE

    Joined:
    Feb 3, 2019
    Posts:
    729
    So close, yet so far...

    Found another Achille's heel when trying to build on top of IJobChunk. The above code will only work if everything is intended to be ReadWrite. If, for example, the 'Bar' IBufferElementData type is ReadOnly, then a [ReadOnly] attribute must be placed on the
    ArchetypeChunkBufferType<Bar>
    inside the Wrapper job.

    This would necessitate so many different permutations of wrapper job types that it makes the whole approach nonviable.

    Back to not having a solution yet...

    ...I am investigating the safety of using
    [NativeDisableContainerSafetyRestriction]
    on ArchetypeChunkComponentType & ArchetypeChunkBufferType here...
     
    Last edited: Apr 3, 2020
  17. RichardWepner

    RichardWepner

    Joined:
    May 29, 2013
    Posts:
    33
    To be honest, I didn't read all of your code snippets. ^^

    From what I understand: You have a job-type that will always receive a certain set of components with always the same type of usage (i. e. readonly or not), with mainly the same code, but some specialties per job.

    Default Interface method implementations (see e. g. this article about it) could be utilized to define all the common behavior in a base interface. This base interface could also define some methods for specialization that would then be implemented by the implementing
    struct
    . Similar to your examples, this base interface would be generic so you can define the job specific type.
    The one major problem about this: it requires C# 8, which isn't currently supported by Untiy. Maybe there will be a release this year that enables C# 8, or maybe not. Further, Burst would need to support default implementations in interfaces, but I honestly don't see a reason why Burst should have troubles with it (troubles as in "can't be implemented", not as in "changes would need to happen").