Bevy ECS Schedule and Change Detection Interoperability
29K reputation · 25 Jun 2026, 15:12 UTC
Bevy utilizes a multi-threaded executor within its Schedule to parallelize systems based on component access locks. A core feature of this architecture is change detection, which allows systems to filter for entities that have been modified since the last run.
In scenarios involving massive entity counts where updates are infrequent, the overhead of tracking these changes across the system boundary may impact performance. There is uncertainty regarding whether the scheduler can bypass change detection checks entirely for specific system sets when no modifications are flagged globally.
Does the current Schedule implementation provide a mechanism to suppress change detection overhead for low-traffic workloads? Can a system be configured to skip its execution entirely if no entities within its query have been flagged as changed by previous systems in the same frame?
1 answer
1 question comment
Use comments to ask for clarification. Post a solution as an answer.
29,025 reputation · 25 Jun 2026, 16:11 UTC
Change Detection is Global, Not Stage‑Local
Bevy’s Changed flag is set the moment a component is mutated via Mut—regardless of which stage performs the mutation. The flag is cleared only by the Reset stage (usually CoreStage::PostUpdate) at the end of the schedule.
Implications for Multi‑Stage Workflows
- All stages that run after a writer will see the same set of changed components until the next
Reset. This means you cannot “skip” a reader by placing it after a writer in the same frame; the reader will still be invoked. - If you need a reader to see a writer’s updates immediately, schedule the writer before the reader and keep the default
Resetat the end. - Adding an intermediate
Resetstage between writers and readers effectively splits the frame into two logical passes, but it also forces a full change‑flag sweep each time.
Practical Verification
app.add_system(writer.in_set('StageA'));
app.add_system(reader.in_set('StageB'));
app.add_stage_after('StageB', 'Reset', Reset::default());
Running the above will show that reader sees the mutation only if writer runs first; inserting Reset after StageB clears flags before the next frame.