@Exclusive systems¶
Most systems are per-entity iterators: the framework selects matching entities and calls your method once per row. @Exclusive turns that off. An exclusive system runs exactly once per tick, takes no component parameters, and receives the World itself as a handle to do bulk work the normal iteration model cannot express.
Use it sparingly — every exclusive system is a parallel-scheduling barrier.
Declaration¶
Annotate the method with @Exclusive. The rest of the method stays a normal @System:
import zzuegg.ecs.system.Exclusive;
import zzuegg.ecs.world.World;
class Bookkeeping {
@Exclusive
@System(stage = "Last")
void resetTickCounters(World world, ResMut<Stats> stats) {
stats.get().reset();
}
}
What you can put in the parameter list:
World— the owning world. The exclusive runner always fills this slot with the world instance.- Any standard service parameter —
Commands,Res,ResMut,EventReader,EventWriter,Local,RemovedComponents,PairReader,ComponentReader. - Not
@Read/@Writecomponent parameters. Exclusive systems have no per-entity iteration to bind them to. - Not
Entity. There is no "current entity" because no entity is being iterated.
If the parser sees component parameters on an @Exclusive method, you'll get a validation error at world-build time.
What "exclusive" means to the scheduler¶
The DAG builder hard-wires exclusive systems against every other system in the same stage. hasConflict returns true whenever either side is exclusive, regardless of declared component or resource access:
The practical effect is that an exclusive system runs alone in its stage. No parallel system runs alongside it. This is the whole point — inside the method, the world is quiescent and you can poke at it without worrying about a neighbour writing to the same data.
Exclusive systems are serialization points
Every @Exclusive system forces the multi-threaded executor to funnel down to one thread for the duration of that system's execution. Putting a hot exclusive system in the middle of Update can wipe out 50% of your parallel gains. Prefer First or Last for bookkeeping.
Tier-1 generated runner¶
Because exclusive systems have such a restricted shape — service-only parameters, no per-entity dispatch — the framework compiles them to a dedicated tier-1 hidden class: GeneratedExclusiveProcessor. The generated bytecode:
- Hoists the pre-resolved service argument array once into a local.
- Loads each slot, casts to the declared parameter type via
checkcast. - Calls the user method with a direct
invokevirtual(orinvokestaticif the method is static).
The win versus the reflective SystemInvoker path is roughly one MethodHandle.asSpreader setup and one MethodHandle.invoke per call — modest because exclusive systems are called at most once per tick per stage. What you actually get is the JIT treating the call as fully monomorphic, which can inline the body into whatever the caller looks like.
The fallback path is SystemInvoker with reflective Method.invoke. Both paths produce identical results; the runner lookup is transparent at executeSystem time.
When to reach for @Exclusive¶
Three patterns justify the barrier cost.
1. Bulk structural edits¶
When you need to visit every entity of a type and issue a structural change — despawn every projectile that left the screen, promote every queued entity into a real archetype — a normal system can use Commands per-entity, but the dispatch overhead of iterating millions of rows is wasted if the work is really "scan a list, apply to the world".
@Exclusive
@System(stage = "Last")
void despawnExpired(World world, ResMut<ExpiryQueue> queue, Commands cmds) {
for (var entity : queue.get().drain()) {
cmds.despawn(entity);
}
}
The exclusive guarantee means no other system is adding to ExpiryQueue while you drain it.
2. Cleanup passes that touch the store directly¶
Some cleanup passes can't be expressed as "iterate entities with component X" — for example, shrinking an internal index or compacting a resource. Take the World reference and call whatever API you need on it.
@Exclusive
@System(stage = "First")
void compactWorld(World world) {
world.debugDump(); // or any world-level API
}
3. Reset-at-end-of-tick resources¶
Resources that are "per-tick" — frame counters, queued inputs, accumulated diagnostics — benefit from an exclusive system at Last that zeroes them after everyone has read. Because the system runs alone, there is no race between "I'm still reading the accumulator" and "I'm clearing it".
@Exclusive
@System(stage = "Last")
void resetFrameStats(World world, ResMut<FrameStats> stats) {
stats.get().clear();
}
Prefer Last for resets, First for setup
Stages run in ascending order, so a cleanup system in Last observes the final state of the tick, and a setup system in First primes the world before anything else runs. Both positions keep the exclusive barrier out of the hot update loop.
Difference from a regular system¶
| Behaviour | Regular @System |
@Exclusive @System |
|---|---|---|
| Called per entity | Yes | No — called once per tick |
| Parameters | Components + services | Services only |
World parameter allowed |
No | Yes, filled with owner |
| Parallel with other systems | Yes, subject to DAG | No — blocks entire stage |
| Tier-1 backing | GeneratedChunkProcessor |
GeneratedExclusiveProcessor |
| Typical use | Game logic | Bookkeeping, bulk edits |
A complete cleanup example¶
public record Expired(long at) {} // marker: this entity should die
public record Tick(long value) {} // resource: current tick count
class Lifetimes {
// Regular system: flag expired entities.
@System
void checkExpiry(@Read Expired expiry, Res<Tick> tick, Entity self, Commands cmds) {
if (tick.get().value() >= expiry.at()) {
cmds.add(self, new PendingDespawn());
}
}
// Exclusive system: scan the pending list and drop them all at end of tick.
// Runs alone in `Last`, so we can safely drain a resource the regular
// systems just finished writing.
@Exclusive
@System(stage = "Last")
void applyDespawns(World world, ResMut<PendingDespawnQueue> queue, Commands cmds) {
for (var entity : queue.get().drain()) {
cmds.despawn(entity);
}
}
}
Note that even an exclusive system uses Commands for the actual despawns — the structural pipeline is still single-threaded and applied at the stage boundary, so letting the framework handle it is simpler than calling world.despawn(...) directly.
What's next¶
- Run conditions — gate systems on a runtime boolean check.
- Related basics: Commands, Resources.