ECS dependency system and job.Complete()

Hello, I have got several questions about dependency handling and systems in ECS. I will be happy if at least some of these question will be answered:

  • How dependency system determines what should be in Dependency property when OnUpdate is called?
    I checked SystemBase class and I noticed some functions registering “read/writes” from query or some methods like GetBuffer() with some additional stuff.

  • Following the first qestion, how to correctly get handles, buffers and similar things? Is EntityManager.GetBuffer valid option instead of this.GetBuffer (this = SystemBase)?

  • There is one system running two jobs. First one writes randomly to buffer A and second reads A, should I get buffer twice, where second one is readOnly = true? So far I noticed this throws error as it counts acces twice or something.

  • How can I acces, or actually pass buffer in systemB if systemA is running job using that buffer? Unity throws error when buffer GetBuffer is called as “it’s read from somewhere”.

  • For some reason in some cases system might enforce Complete() when both of them write to the same data. This does not make sense at all and I don’t understand why this happens. Obviously we are not talking about job completed from previous frame.

  • Hybrid renderer is not used, but forces complete of all my jobs for no reason. That package has one query with 4 components (match ALL) and one of them is LocalToWorld (or LocalToParent?). My job is using it too, but it should not be matched by hybrid renderer, because entity does not have other required components. Does it mean that dependency collects everything even when it’s not used or matched?

  • There is [ReadOnly] attribute, but also methods can get readOnly parameter. Do I need to use both of them to get it read only? If not, then what is the point of attribute or that parameter?

  • Do I need to assign all created jobs to dependency directly or such a job must be contained in the chain?
    Are both of these example valid?

// Not actual C# code

// Example 1
var handleA = JobA.Schedule(Dependency);
var handleB = JobB.Schedule(handleA);
Dependency = JobC.Schedule(handleB);

// Example 2
var handleA = JobA.Schedule(Dependency);
var handleB = JobB.Schedule(Dependency);
var handleC = JobC.Schedule(Dependency);
Dependency = Combine([handleA, handleB, handleC]);
1 Like

They are registered via calls to SystemBase.GetFromEntity() and SystemBase.GetTypeHandle()

For the purposes of this discussion, pretend SystemBase.GetBuffer() does not exist. We can revisit it once you understand the fundamentals.

Use the proper accessor handles API from (1) when dealing with jobs.

That’s because that GetBuffer() call may have actually been happening on the main thread.

No. But there is some query that is matching. And because it is matching, the system runs. And because it runs, it forces complete on all jobs involving LocalToWorld.

One is used to fetch the right JobHandle. The other is used to ensure job system safety.

Yes

So does it mean if I don’t use them, then Dependency system have no clue and might throw error that two jobs access the same data, correct?

By fundamentals you mean? I don’t quite understand this answer.

I did use GetBuffer one with readonly and one without it to feed two chained jobs, but unity was not happy about that. A bit more context in next point below

This is right, but I am not sure how to solve it in my case: There are some entities grouped in parent and this parent has dynamic buffer. There is job for child entities, but they need access to that “external” bufer they don’t own.

Hybrid system has some attribute execute always or something and it completes Dependency when it runs, however that would mean that it’s Dependency contains my jobs, but there is no way any of these queries could match (there are 4).
I will double check tomorrow.

By fetch the right handle you mean to disallow scheduling two jobs with the same resource? I mean it sounds like both of these things are the same, but I understand it’s used by two separate systems, first one by Job system, and second one by ECS Dependency.

By fundamentals, I mean SystemBase and IJobEntityBatch. No Entities.ForEach(). No Job.WithCode(). No IJobEntity. All those other things get codegenned into SystemBase + IJobEntityBatch. So if you want to understand how everything works, you have to look at those fundamentals.

Fundamentally, yes. But with codegen it might be auto-generated for you already.

Forget GetBuffer(). It is messing you up. Use BufferFromEntity or BufferTypeHandle instead.

One is used as part of the job system DAG generation. The other is used by a safety system and serves no purpose when the safety system is disabled.

1 Like

I don’t use those loop helpers and instead IJobEntityBatch. I have seen in docs that loop is updating Dependency automaticaly, however this system is not 100% clear for me, because if I understand correctly, when OnUpdate starts that Dependency property should contain already all jobs needed to run my system, but this system didn’t schedule anything yet, so it’s impossible to know. In addition I tried to read some internals and there is some strange system to complete handle when it already contains some dependency in those read write lists.

I will definitely try this.

The last part I don’t understand is why some systems force job completion. Is there way to debug this? I try to check system and query window, but it shows 0 selected entities, so I don’t get why it’s completed.

On the first run, if it encounters a type handle it hasn’t registered yet, it will complete any job dependencies associated with that type on the spot. In subsequent runs, it will look at the registered types and and combine the handles without completing them. By the nature of the mechanism, you tend to get a lot more sync points on the first frame while the mechanism registers everything in a lazy fashion.

The Hybrid Renderer is a special case with a lot of details I don’t really want to explain. I have a modified Hybrid Renderer, and I am closer to having that forced completion go away. But I’m not there yet.

1 Like

I have the dependency chains pretty similar to the example code in the OP, but now I try to use IJobEntity, and I do not understand how should I get the JobHandle.

Update…

Ok, so you can do ScheduleParallel(entityQuery, state.Dependency);

Somehow I didn’t pass them both at the same time correctly before…

In the example of its usage there is a very silly situation when you get the position data and dispose of it right away:

protected override void OnUpdate()
{
   // Get a native array equal to the size of the amount of entities found by the query.
   var positions = new NativeArray<float3>(query.CalculateEntityCount(), World.UpdateAllocator.ToAllocator);

   // Schedule job on parallel threads for this array.
   new CopyPositionsJob{copyPositions = positions}.ScheduleParallel();

   // Dispose the array of positions found by the job.
   positions.Dispose(Dependency);
}

The funny thing that it seems you can do nothing about that NativeArray but Dispose, because if you try to actually use these data, you will have an error:
InvalidOperationException: The previously scheduled job CopyPositionsJob writes to the Unity.Collections.NativeArray1[Unity.Mathematics.float3] CopyPositionsJob .JobData.positions. You must call JobHandle.Complete() on the job CopyPositionsJob , before you can read from the Unity.Collections.NativeArray1[Unity.Mathematics.float3] safely.

Somehow the code below can not convert “void” to “Unity.Jobs.JobHandle”, IJobEntity does not return JobHandle the way I expect.

JobHandle jobHandle = new CopyPositionsJob{copyPositions = positions}.ScheduleParallel();

So how do you get the JobHandle and make the example in tutorial actually work?

P.S. Maybe I should’ve created the new topic, but this one is very close to my question IMHO.

If you don’t use a JobHandle in ScheduleParallel, it defaults to using state.Dependency and will write the job’s new handle back to state.Dependency and not return anything. If you do pass in a JobHandle, then the method returns the JobHandle and you have to work with that.

1 Like

I have a related question…

You say these are equivalent:

// Example 1
var handleA = JobA.ScheduleParallel(state.Dependency);
var handleB = JobB.ScheduleParallel(handleA);
state.Dependency = JobC.ScheduleParallel(handleB);

// Example 2
var handleA = JobA.ScheduleParallel(state.Dependency);
var handleB = JobB.ScheduleParallel(state.Dependency);
var handleC = JobC.ScheduleParallel(state.Dependency);
state.Dependency = Combine([handleA, handleB, handleC]);

Is this one also equivalent?

// Example 3
state.Dependency = JobA.ScheduleParallel(state.Dependency);
state.Dependency = JobB.ScheduleParallel(state.Dependency);
state.Dependency = JobC.ScheduleParallel(state.Dependency);

I saw someone using it that way to chain jobs in the forums once and I was sceptical at first, but my current impression is that it will work because I think the accessor for state.Dependency automatically appends each dependency.

Example 2 is only identical to example 1 in that the resulting state.Dependency will hold a JobHandle representing the completion of all three jobs. Otherwise, example 2 is different from example 1 because the jobs are allowed to run in parallel to each other, whereas in example 1 they must run sequentially.

Example 3 is identical to example 1, except there might be an miniscule amount more overhead due to the extra branches inside state.Dependency property implementations, but it might also be that Burst sees right through that and removes the branches. I haven’t looked at what Burst is doing there recently.

2 Likes

So it would be more efficient to create our own handles instead of overwriting state.Dependency?

set: I can see a ternary operator coming from SetFlag in NeedToGetDependencyFromSafetyManager
get: The branch if (NeedToGetDependencyFromSafetyManager) that is always skipped in OnUpdate.

Did I miss any? Can Burst even do something about it? I don’t think so.