-
Input C# code will be in the format of:
-
void override OnUpdate(JobHandle jobHandle)
-
{
-
float dt = Time.deltaTime;
-
return Entities
-
.WithNone()
-
.WithBurst(maybe)
-
.ForEach((ref Position p, in Velocity v) =>
-
{
-
p.Value += v.Value * dt;
-
})
-
.Schedule(jobHandle);
-
}
-
This syntax is nice, but the way the C# compiler generates code for it is not great for us. The code the compiler creates looks like this:
-
JobHandle override OnUpdate(JobHandle jobHandle)
-
{
-
var displayClass = new DisplayClass()
-
displayClass.dt = Time.deltaTime;
-
var tempValue = EntitiesForEach;
-
tempValue = tempValue.WithNone();
-
tempValue = tempValue.WithBurst(true);
-
var mydelegate = new delegate(displayClass.Invoke, displayClass);
-
tempValue = tempValue.ForEach(mydelegate);
-
return tempValue.Schedule(jobHandle);
-
}
-
class DisplayClass
-
{
-
public void dt;
-
public void Invoke(ref Position p, in Velocity v)
-
{
-
p.Value += v.Value * dt;
-
}
-
}
-
The first thing to note is that because the lambda expression captures (=uses) the local variable dt, and the c# compiler does not try to convince
-
itself that the delegate does not live longer than OnUpdate() method, the c# compiler decides "okay, we cannot store variable dt on the stack like we always
-
do, because someone might hold on to the delegate, and invoke it 10 minutes from now, and they still need to be able to access variable dt". So what it
-
does is it creates a separate class that it names DisplayClass. It instantiates a single instance of that at the beginning of the function, and any local variable
-
that the compiler needs to ensure can stay around for longer than this stackframe is alive, gets stored in the displayclass instead. From that point on, any normal
-
reads and writes to that local variable will read/write to the field of that single displayclass instance.
-
Also note that the code of the lambda expression was turned into a method that lives on this DisplayClass, so that the code can easily access these captured variables.
-
The good news is that the compiler does a lot of the heavy lifting for us. It already figured out all the variables the lambda expression wants to read from. But
-
the bad news is that the code it generated causes a heap allocation of this DisplayClass. This is especially sad, because when we do “escape analysis” in our head
-
for the mydelegate variable that holds our delegate, we can easily see that it “doesn’t escape anywhere”, so this worry of it being invoked 10 minutes from now is invalid.
-
This re-writer will take the output of the c# compiler as described above, and make a series of changes to it:
-
- We will change DisplayClass to be a struct instead of a class, to avoid the heap allocation that we can see is not required anyway.
-
(this requires changing the DisplayClass type itself, but also the IL of the OnUpdate() method, as the IL that instantiates a heap object, and reads/writes to its field
-
is different from the IL that you need to do the same to a struct that lives in a local IL variable.
-
- Note that the DisplayClass struct almost looks like a manually written job struct. Unfortunately we cannot simply change it a bit more to be like a job struct, as
-
it’s possible and very likely that the OnUpdate() method has another lambda expression somewhere, and that one will also be placed in the same DisplayClass, and our job
-
system does not support using the same class for different jobs.
-
So, too bad, we need to make a new custom struct for our job. We will do that, and copy all the relevant parts of DisplayStruct that we need:
-
- we copy all the fields that DisplayClass.Invoke() uses,
-
- we copy the Invoke method itself, and patch all the IL instructions to not refer to DisplayClass’s fields, but to the new job structs’ fields.
-
- we make this new struct implement ICodeGeneratedJobForEach<T1,T2>
-
- we replace the IL in OnUpdate() that creates the delegate, with IL that initializes a value of this new jobstruct, and with IL that populates all of the fields
-
in the custom job struct with the values that are in the display class.
-
- Since scheduling an ECS job requires a EntityQuery, and we can easily statically see what the query should be, this is code-generated automatically. We inject
-
a GetEntityQuery() call in the system’s OnCreate method, and use it in the final Schedule call.
-
- We try very hard to keep as much code as possible in handwritten form, and require as little as possible code-generated code. Take a look at ICodeGeneratedJobForEach and
-
WrappedCodeGeneratedJobForEach implementations to get an idea of all the handwritten code that works hand-in-hand with this code-generated code. We cannot escape some IL
-
code-generation though, and the ICodeGeneratedJobForEach<Position,Velocity> interface that we make our job implement requires us to also implement ExecuteSingleEntity.
-
We codegen that and have it invoke the user’s original code which still lives in Invoke(). We also massage the arguments here a little bit. Notice how we pass position
-
by ref, but velocity not by ref, as the users’ code asked for velocity through “in”.
-
- Finally, we codegen a Schedule() call, directly on the generated struct itself (turns out this was easier than the traditional pattern of using an extension method).
-
We use the handwritten WrappedCodeGeneratedJobForEach struct, which is an IJobChunk job itself. We initialize it by embedding our own job data inside of it, and setting
-
the readonly values for each element. after that we “just schedule the wrapper as a normal IJobChunk”.
-
The final generated code looks roughly like this:
-
void override OnCreate()
-
{
-
_newJobQuery = GetEntityQuery_ForNewJob_From(this);
-
}
-
static EntityQuery GetEntityQuery_ForNewJob_From(ComponentSystemBase componentSystem)
-
{
-
return componentSystem.GetEntityQuery(new EntityQueryDesc() {
-
All = ComponentType.ReadWrite(), ComponentType.ReadOnly() }
-
None = ComponentType.ReadOnly()
-
});
-
}
-
JobHandle override OnUpdate(JobHandle inputDependencies)
-
{
-
var displayClass = new DisplayClass()
-
displayClass.dt = Time.deltaTime;
-
var tempValue = EntitiesForEach;
-
tempValue = tempValue.WithNone();
-
tempValue = tempValue.WithBurst(true);
-
var newjob = new NewJob();
-
newjob.ScheduleTimeInitialize(this, ref displayClass);
-
return newjob.Schedule(this, _newJobQuery, inputDependencies);
-
}
-
struct DisplayClass
-
{
-
public void dt;
-
public void Invoke(ref Position p, in Velocity v)
-
{
-
p.Value += v.Value * dt;
-
}
-
}
-
struct NewJob : ICodeGeneratedJobForEach<ElementProvider_IComponentData.Runtime, ElementProvider_IComponentData.Runtime>
-
{
-
public void dt;
-
public void Invoke(ref Position p, in Velocity v)
-
{
-
p.Value += v.Value * dt;
-
}
-
public void ExecuteSingleEntity(int indexInChunk, ElementProvider_IComponentData.Runtime runtime0, ElementProvider_IComponentData.Runtime runtime1)
-
{
-
Invoke(ref runtime0.For(indexInChunk), runtime1.For(indexInChunk);
-
}
-
public void Schedule(EntityManager entityManager, EntityQuery entityQuery)
-
{
-
WrappedCodeGeneratedJobForEach<NewJob,
-
ElementProvider_IComponentData, ElementProvider_IComponentData.Runtime,
-
ElementProvider_IComponentData, ElementProvider_IComponentData.Runtime> wrapper;
-
wrapper.wrappedUserJob = this;
-
wrapper.Initialize(entityManager, readonly0: false, readonly1: true);
-
wrapper.Schedule(entityQuery); //<-- wrapper is an IJobChunk, and this is a regular IJobChunk schedule call
-
}
-
}
*/