Add observer ordering via ObserverSet and topo-sorted dispatch#24328
Add observer ordering via ObserverSet and topo-sorted dispatch#24328caniko wants to merge 13 commits into
Conversation
Foundation for observer ordering (bevyengine#14890). No behaviour change yet — descriptor edges are unread by storage and dispatch; that wiring lands in later commits.
All four dispatch sites in event/trigger.rs now route through a single ordered dispatch helper over sorted NodeId streams. Archetype-flag fast skip is preserved. Cross-bucket ordering is now observable at runtime when observer ordering edges exist.
|
Welcome, new contributor! Please make sure you've read our contributing guide, as well as our policy regarding AI usage, and we look forward to reviewing your pull request shortly ✨ |
|
It looks like your PR has been selected for a highlight in the next release blog post, but you didn't provide a release note. Please review the instructions for writing release notes, then expand or revise the content in the release notes directory to showcase your changes. |
|
run_if already works now :) Add a test for the interaction? |
|
Added follow-up commits completing the public API surface to match SystemSet's shape:
Storage and dispatch are unchanged from the previous commits; this is additive on the builder and diagnostics layer. The new observer tests cover tuple chaining, tuple-wide modifiers, set chaining, naming, and filtered dispatch-order accessors. I also pushed a small follow-up formatting commit after CI caught stable clippy's |
Add observer ordering via
ObserverSetand topo-sorted dispatchResolves #14890.
What this changes
Observers can be ordered against each other using a
SystemSet-style API:Cross-bucket ordering works natively: a global observer can be ordered against a per-entity or per-component observer. The implementation uses one topo sort per event key, then reuses the sorted order across all dispatch sites. The existing archetype-flag fast skip is preserved.
Approach
ObserverSettrait + derive, mirroringSystemSet. Identities are stored asInterned<dyn ObserverSet>.CachedObserversreplaces the oldEntityHashMapbuckets with one node table, one topo-sortedorder, and sorted inverted indices per bucket.bevy_ecs::schedule::graph::DiGraph, so observers and systems share the same ordering kernel.event/trigger.rsgoes through onerun_orderedhelper that performs a k-way merge over sortedNodeIdstreams. No per-dispatch allocation.Breaking changes
ObserverMaptype alias removed. Replaced by indexed-Vec storage. Downstream code readingworld.observers()to enumerate observers needs to use a new iterator API. There are very few such users in the ecosystem (mostly debug tools).CachedObservers/CachedComponentObserversfield shapes change. The fields were already private; only the accessors (global_observers(), etc.) were public. The new accessors return sorted slices ofNodeIdplus anodes()table.ObserverSet. Strictly stronger than today's contract; nothing correct can break, but anything that relied on undefined order (test asserts, frame replays) will need updates. Call out in the PR.event/trigger.rschange shape. Internal, noTriggertrait signature changes, but customTriggerimpls in the wild (Bevy picking is the largest) need a quick port to the newrun_orderedhelper. Bevy maintainers can drive this in the same PR.What's NOT in this PR
SystemParamaccess analysis per observer to build a conflict graph..run_if(condition)on observers. Adjacent, but kept out to constrain review scope.Traversal; per-hop dispatch now uses the sorted observer order.ambiguous_withfor observers. Only meaningful once parallel observer execution exists.Design Notes
Design summary
The key design choice is one event-local observer graph, not four separately sorted buckets. This enables edges across dispatch buckets while avoiding four topo sorts per event. Each registered observer/event pair becomes an
ObserverNodewith a stable insertion sort key.CachedObserversstores a dense node table, an event-wide sortedorder, and bucket indices containingNodeIds sorted by that event-wide order.Dispatch sites pass the relevant sorted streams into
run_ordered. For one stream, dispatch is a direct dense slice walk. For multiple streams without ordering edges, dispatch preserves existing bucket order without allocation. If ordering edges exist,run_orderedwalks the event-wide order and runs nodes present in the active streams, which gives cross-bucket ordering without allocating a merged list.Archetype flags remain the fast skip for lifecycle component observers. Register/unregister returns the same component-observer deltas needed to update those flags, now backed by the new component bucket indices.
Test coverage
New and updated
bevy_ecsobserver coverage includes:ObserverSetderive and set membership.dispatch_order_foraccessor coverage.Discardlifecycle terminology.Local validation
cargo check --workspace: passed.cargo test -p bevy_ecs --doc: passed.cargo clippy -p bevy_ecs -- -D warnings: passed.cargo test -p bevy_ecs: observer tests pass, but the full suite is blocked locally byerror::bevy_error::tests::filtered_backtrace_test. I verified the same test fails on untouched upstreammain(370be1b02) under Rust 1.95.0 with the same assertion, so this is not caused by this branch.