m04 · Jobs · reading · 9 min
IJobEntity and IJobChunk — the chunk becomes the work unit
Independence you already have
The last two modules kept promising this payoff, so let’s collect it.
So the natural unit of parallel distribution is the chunk. Hand chunk 0 to core 0, chunk 1 to core 1, and so on; each core runs the same per-entity logic over its own chunk’s dense arrays, and because chunks don’t overlap, there is nothing to race over. The 16 KB chunk size from Module 1 was chosen partly for this: it’s a clean, bounded unit of work to hand a thread — big enough to amortize scheduling overhead, small enough to balance load across cores when entity counts are uneven.
IJobEntity: write per-entity, run per-chunk
IJobEntity is the ergonomic way to iterate entities in a job. You write
what reads like a per-entity function — an Execute that takes the
components of one entity — and the source generator turns it into a job
that iterates chunks:
[BurstCompile]
partial struct MoveJob : IJobEntity
{
public float DeltaTime;
void Execute(ref LocalTransform transform, in Velocity velocity)
{
transform.Position += velocity.Value * DeltaTime;
}
}
// scheduled across cores in one call:
state.Dependency = new MoveJob { DeltaTime = dt }.ScheduleParallel(state.Dependency);
Read the two layers. The Execute you wrote describes one entity:
here’s a transform, here’s a velocity, integrate. But ScheduleParallel
runs it as per-chunk parallel work — the generated job walks matching
chunks, and the scheduler distributes those chunks across worker threads,
each thread calling your Execute for every entity in its chunk. You
expressed the logic at the entity level; the system executed it at the
chunk level across cores. This is the same separation as always: you say
what happens to an entity, the runtime decides how it’s spread over
hardware.
The parameter annotations carry the access declaration the safety system
needs: ref LocalTransform says this job writes transforms, in Velocity
says it only reads velocity (the in is the per-parameter equivalent of
[ReadOnly]). From those, the scheduler knows this job conflicts with
anything else writing LocalTransform and can run concurrently with other
readers of Velocity — the entire safety analysis from the last lesson,
driven by how you declared each component parameter.
IJobChunk: the level underneath
IJobEntity is generated on top of a lower-level interface, IJobChunk,
whose Execute receives a whole chunk at once — the archetype chunk
object, from which you pull the component arrays and loop yourself:
[BurstCompile]
struct MoveChunkJob : IJobChunk
{
public ComponentTypeHandle<LocalTransform> TransformHandle;
[ReadOnly] public ComponentTypeHandle<Velocity> VelocityHandle;
public float DeltaTime;
public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex,
bool useEnabledMask, in v128 chunkEnabledMask)
{
var transforms = chunk.GetNativeArray(ref TransformHandle);
var velocities = chunk.GetNativeArray(ref VelocityHandle);
for (int i = 0; i < chunk.Count; i++)
transforms[i] = /* integrate transforms[i] with velocities[i] */ transforms[i];
}
}
This is more verbose, and most of the time IJobEntity is the right
choice — it generates essentially this and spares you the handles and the
loop. But IJobChunk gives you the chunk itself, and that unlocks things
the per-entity view hides: operating on the component arrays as arrays
(a manual SIMD pass, a bulk memcpy), reading chunk-level metadata, custom
handling of the enabled-mask for enableable components, or any logic that
is genuinely about the chunk rather than one entity. You drop to this
level when you need the chunk as a first-class thing; you stay at
IJobEntity when you’re really just processing entities.
What this buys you
You can now see why the chunk is the unit parallelism was waiting for, how
IJobEntity presents entity-level code that executes as chunk-level
parallel work, and when the lower-level IJobChunk is worth its
verbosity. What remains is the connective tissue: how jobs declare that
one must finish before another, how JobHandle chains them into a
dependency graph, and how that graph relates to the sync points from
Module 2. That’s the next lesson.