Skip to content

Jobs And Scheduling

Job Types

flowchart LR
subgraph ImplicitDependency
    Job.WithCode
    IJobEntity
    IJobParallelFor
    IJobParallelForTransform
end
subgraph ExplicitDependency
    IJobChunk
end
subgraph Deprecating
    Entities.ForEach
end
style Entities.ForEach fill:red

Code Generation

flowchart LR
    IJobEntity --> IJobChunk
    Entities.ForEach --> IJobChunk
    style Entities.ForEach fill:red

Jobs And Systems

flowchart LR
    subgraph System1
        Job1
        Job2
    end
    subgraph System2
        Job3
    end

Implicit vs Explicit Dependency

// implicit
new Job{}.Schedule();
// explicit
var handle = new Job{}.Schedule(dependency);

Implicit

flowchart LR
    subgraph NewlyScheduledJob
        ABC{{ABC}} --> Job --> XYZ{{XYZ}}
    end
    subgraph PrevScheduledJobs
        subgraph Group1
            WriteToA
            WriteToB
            WriteToC
        end
        subgraph Group2
            ReadOrWriteToX
            ReadOrWriteToY
            ReadOrWriteToZ
        end
        AnotherJob
    end
    Group1 --> NewlyScheduledJob
    Group2 --> NewlyScheduledJob
flowchart LR
    subgraph System1
        JobAB
        JobAC
    end
    subgraph System2
        JobBC
    end
    CompA{{CompA}} --> JobAB
    JobAB --> CompB{{CompB}}
    CompA --> JobAC --> CompC{{CompC}}
    CompB --> JobBC --> CompC
- This is what implicit dependency wants to achieve, IN THEORY - IN REALITY, dependencies are automatically managed per system, not per job - Within a system, all implicitly scheduled jobs form a sequential chain (link)
// implicit
new Job{}.Schedule();
// would translate to
Dependency = new Job{}.Schedule(Dependency);
// implicit
new JobAB{}.Schedule();
new JobAC{}.Schedule();
// would translate to
Dependency = new JobAB{}.Schedule(Dependency);
Dependency = new JobAC{}.Schedule(Dependency);
// which means
var handleAB = new JobAB{}.Schedule(Dependency);
Dependency = new JobAC{}.Schedule(handleAB);
flowchart LR
    subgraph System1
        JobAB --> JobAC
    end
    subgraph System2
        JobBC
    end
    PrevFrame{{Prev Frame}} --> System1
    System1 --> ABC{{AB->C}} --> System2
    System2 --> NextFrame{{Next Frame}}
- To fully utilize implicit dependency, one system should only schedule one job
flowchart LR
    subgraph System1
        JobAB
    end
    subgraph System2
        JobAC
    end
    subgraph System3
        JobBC
    end
    PrevFrame{{Prev Frame}} --> System1
    PrevFrame --> System2
    System1 --> AB{{A->B}}
    System2 --> AC{{A->C}}
    AB --> System3
    AC --> System3
    System3 --> NextFrame{{Next Frame}}

Explicit

Are required for:

flowchart LR
    IJobChunk
flowchart LR
    Job1 -- write --> ContainerA{{Container A}} -- read --> Job2
    Job1 -- need explicit --> Job2
Code example:
    var array = new NativeArray(Allocator.TempJob);
    var handle1 = new Job1{
        Output = array,
    }.Schedule(Dependency);
    Dependency = new Job2{
        Input = array,
    }.Schedule(handle1);
    array.Dispose(Dependency);

Schedule vs ScheduleParallel

flowchart TB
    subgraph AlwaysScheduleParallel
      IJobParallelFor
      IJobParallelForTransform
    end
    subgraph HasScheduleParallel 
        IJobEntity
        IJobChunk
    end
    subgraph NoScheduleParallel 
        Job.WithCode
    end
- ScheduleParallel will split execution of the job to multiple Worker Threads - The Job completes when all threads of its execution complete

IJobParallelFor

struct AddJob: IJobParallelFor
{
    public NativeArray<float> values;
    public float increase;

    public void Execute (int index)
    {
        values[index] += increase;
    }
}

var array = new NativeArray<float>(Allocator.TempJob);
...
Dependency = new AddJob{
    values = array,
    increase = 1,
}.Schedule(array.Length, innerLoopBatchCount: 4, Dependency);
array.Dispose(Dependency);
- innerLoopBatchCount should be in power of 2, to take advantage of vectorization when BurstCompile is used - Expensive job could use smaller innerLoopBatchCount
flowchart LR
    Schedule["Schedule(length:8, batch:2)"]
  Schedule --> Batch01
  Schedule --> Batch23
  Schedule --> Batch45
  Schedule --> Batch67
  subgraph NativeJob0
    Batch01
    Batch23
  end
  subgraph NativeJob1
    Batch45
    Batch67
  end
  NativeJob0 --> Core0
  NativeJob1 --> Core1

IJobEntity

partial struct DoubleJob: IJobEnttiy {
    private void Execute(in CompA compA, ref CompB compB) {
        compB.Value = 2 * compA.Value;
    }
}
new DoubleJob{}.ScheduleParallel();
- IJobEntity has an implicit query derived from the Execute method - Behind the scene it is compiled into IJobChunk
flowchart LR
    DoubleJob --> Query
    Query --> Chunk0 --> Core0
    Query --> Chunk1 --> Core1
- Use EntityIndexInQuery to fill values for newly created entities. This is similar to IJobParallelFor, but is more memory efficient because entities are processed in chunks.
partial struct FillJob: IJobEntity {
    public NativeArray<float> Inputs;
    private void Execute(
        [EntityIndexInQuery] int index, 
        ref CompA compA){
        compA.Value = Inputs[index];
    }
}

var values = new NativeArray<float>(100, Allocator.TempJob);
new SomeJob{
    Outputs = values,
}.ScheduleParallel();
EntityManager.Instantiate(prefab, inputs.Length, Allocator.Temp);
new FillJob{
    Inputs = values,
}.ScheduleParallel();

IJobChunk

  • IJobEntity doesn't support Generic, making reusing code not possible
  • Use IJobChunk in this case

interface IComp : IComponentData {
    float GetComputedValue();
}
abstract partial class AbstractSystem<TComp> : SystemBase
    where TComp: unmanaged, IComp
{
    private struct Job : IJobChunk
    {
        [ReadOnly] public ComponentTypeHandle<TComp> Inputs;
        public ComponentTypeHandle<FloatComp> Outputs;

        public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask,
            in v128 chunkEnabledMask)
        {
            var inputs = chunk.GetNativeArray(ref Inputs);
            var outputs = chunk.GetNativeArray(ref Outputs);

            var enumerator = new ChunkEntityEnumerator(useEnabledMask, chunkEnabledMask, chunk.Count);
            while (enumerator.NextEntityIndex(out var i))
            {
                var input = inputs[i];
                outputs[i] = input.GetComputedValue();
            }
        }
    }

    private EntityQuery _query;

    protected override void OnCreate()
    {
        _query = EntityManager.CreateEntityQuery(new EntityQueryDesc
        {
            All = new ComponentType[] { typeof(TComp), typeof(FloatComp) },
        });
    }

    protected override void OnUpdate()
    {
        Dependency = new Job
        {
            Inputs = SystemAPI.GetComponentTypeHandle<TComp>(true),
            Outputs = SystemAPI.GetComponentTypeHandle<FloatComp>(),
        }.ScheduleParallel(_query, Dependency);
    }
}
To use this system: - Implement the component interface - Derive from the abstract system
struct Add: IComp {
    public float A;
    public float B;
    public float GetComputedValue() => A + B;
}

struct Mul: IComp {
    public float A;
    public float B;
    public float GetComputedValue() => A * B;
}

internal partial class AddSystem: AbstractSystem<Add>{}
internal partial class MulSystem: AbstractSystem<Mul>{}

CompleteDependency

// this will wait until ALL previously scheduled jobs finished
Dependency.Complete();
Avoid doing this to increase parallelism

Structural Changes

// will automatically call Dependency.Complete before execute
EntityManager.CreateEntity
EntityManager.Instantiate
EntityManager.DestroyEntity
EntityManager.AddComponent
EntityManager.RemoveComponent
EntityManager.SetSharedComponent

ecb.Playback(EntityManager) //if the ecb is not empty and constains one of the above    
Doing these at the beginning or end of frame to maximize parallelism