Dependency Tracking¶
Trecs tracks which jobs read and write which data and inserts the right JobHandle dependency chain for you. Normally you should not need to call JobHandle.CombineDependencies or make use of JobHandle values — the framework automatically schedules your job based on the Read / Write access you use.
Why this exists¶
Unity's job system makes dependency wiring your problem: every job needs the right input handle. Get it wrong and you get race conditions, or jobs running in sequence when they could have run in parallel. Trecs reads your access pattern (parameter in/ref modifiers, the native field types listed in the thread-safety cheat sheet) and emits the wiring at schedule time.
The reader/writer model¶
Each piece of data follows a standard reader/writer rule:
- Multiple readers run in parallel.
- A writer is exclusive — it waits for all current readers and the previous writer to finish before starting.
Granularity is per (component type, group). A job writing Position for Fish entities does not block a job reading Position for Player entities — different groups. (See Groups & TagSets.)
| Job A | Job B | Run in parallel? |
|---|---|---|
Reads Position (Fish) |
Reads Position (Fish) |
Yes |
Reads Position (Fish) |
Writes Position (Player) |
Yes — different group |
Reads Position (Fish) |
Writes Position (Fish) |
No — writer waits |
Writes Position (Fish) |
Writes Position (Fish) |
No — writer waits |
Same rule for sets: NativeSetRead / NativeSetCommandBuffer are tracked per (set type, group), just like components. Note that deferred set ops on WorldAccessor / NativeWorldAccessor don't need to synchronize — they apply at submission, after all outstanding jobs complete.
How dependencies get declared¶
The source generator inspects each job:
- Iteration parameters:
in TreadsT,ref TwritesT. - Native fields / parameters: the type itself encodes intent (
NativeComponentBufferRead<T>vsNativeComponentBufferWrite<T>, etc.).
The generated ScheduleParallel:
- Combines the
JobHandles of every outstanding conflicting job and passes the result as the new job's input dependency — the new job waits, but the main thread doesn't block. - Registers the new job so subsequent schedules see it as outstanding.
You call only the generated ScheduleParallel method.
Main-thread sync¶
Main-thread access through WorldAccessor lazily completes only the conflicting jobs:
.Read— completes outstanding writers (readers keep running)..Write— completes outstanding writers and readers.
// Completes jobs currently writing Position for this group;
// jobs that only read Position keep running.
ref readonly var pos = ref handle.Component<Position>(world).Read;
// Completes jobs reading OR writing Position for this group.
ref var posMut = ref handle.Component<Position>(world).Write;
That lazy sync is why you never call JobHandle.Complete() yourself — touching the data is the sync point.
Tip
Main-thread access mid-phase forces conflicting in-flight jobs to complete, blocking the main thread until they finish. Minimize these sync points by pushing main-thread reads/writes into a job, or by running them later in the frame so the job has more time to finish.
Phase boundaries¶
Each of the five update phases — EarlyPresentation, Input, Fixed, Presentation, LatePresentation — ends with a full job fence: every outstanding job completes before the next phase begins. So:
- Fixed-phase jobs finish before any presentation system runs.
- Within a phase, mix job and main-thread systems freely — the tracker orders them.
Summary¶
| Mechanism | When | What it does |
|---|---|---|
Generated ScheduleParallel |
Job scheduling | Chains the new job's JobHandle behind conflicting jobs (no main-thread wait); registers new access |
.Read |
Main-thread component access | Completes outstanding writers |
.Write |
Main-thread component access | Completes outstanding writers + readers |
| Phase boundary | Between update phases | Completes everything |