Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

run_if for SystemConfigs via anonymous system sets #7676

Merged
merged 5 commits into from
Mar 30, 2023

Conversation

geieredgar
Copy link
Contributor

@geieredgar geieredgar commented Feb 14, 2023

Objective

Solution

The idea of anonymous system sets or "implicit hidden organizational sets" was briefly mentioned by @cart here: #7634 (comment).

  • Schedule::add_systems creates an implicit, anonymous system set of all systems in SystemConfigs.
  • All dependencies and conditions from the SystemConfigs are now applied to the implicit system set, instead of being applied to each individual system. This should not change the behavior, AFAIU, because before, after, run_if and ambiguous_with are transitive properties from a set to its members.
  • The newly added AnonymousSystemSet stores the names of its members to provide better error messages.
  • The names are stored in a reference counted slice, allowing fast clones of the AnonymousSystemSet.
  • However, only the pointer of the slice is used for hash and equality operations
    • This ensures that two AnonymousSystemSet are not equal, even if they have the same members / member names.
      • So two identical add_systems calls will produce two different AnonymousSystemSets.
    • Clones of the same AnonymousSystemSet will be equal.

Drawbacks

If my assumptions are correct, the observed behavior should stay the same. But the number of system sets in the Schedule will increase with each add_systems call. If this has negative performance implications, add_systems could be changed to only create the implicit system set if necessary / when a run condition was added.

@github-actions
Copy link
Contributor

Welcome, new contributor!

Please make sure you've read our contributing guide and we look forward to reviewing your pull request shortly ✨

@alice-i-cecile
Copy link
Member

Oh very clever. I'll do a full review and add this to the milestone. Welcome and thank you!

@alice-i-cecile alice-i-cecile added A-ECS Entities, components, systems, and events C-Usability A targeted quality-of-life change that makes Bevy easier to use labels Feb 14, 2023
@alice-i-cecile alice-i-cecile added this to the 0.10 milestone Feb 14, 2023
Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally very impressed. I'd like to see some basic tests for hashing and equality added, based on the behavior in the PR description.

@cart
Copy link
Member

cart commented Feb 14, 2023

Haven't done a full review yet, but I do think this is probably the right path forward. But I'm very curious what others think (especially @maniwani, given their work in this area).

First some pros:

  1. It makes run conditions "more optimal" by only running them once for a group of systems. (also a con in some contexts).
  2. It makes it possible to express (foo, bar).run_if(X), which might help make removing OnUpdate possible / desirable.
  3. It makes our graphs smaller and therefore faster to "crunch". If we have 100 systems all sharing a after(X) constraint, thats 99 fewer edges in the graph.

Some cons:

  1. This is very implicit. There is a behavioral difference between .add_system(foo.run_if(X)).add_system(bar.run_if(X)) vs .add_systems((foo, bar).run_if(X)), which will be more pronounced the more "separate" foo and bar are in a schedule.
  2. This makes "schedule visualization" more difficult as rendering these set names could be very unwieldy for large numbers of systems in a set.
  3. As already mentioned, this has the potential to create a lot of "useless" sets. I think we should avoid creating these sets when they aren't needed (ex: they dont have before/after/run_if/in_set).
  4. Idk if including the system names in the anonymous sets is ideal. We can already do "set membership" calculations elsewhere if we need this info, and any "set debug" view will already show the members of systems in a set. Additionally, it is a O(N) allocation that is only useful in a debug context. Probably better to just give anonymous sets ascending ids or something. Ex: AnonymousSet(5). We should double check that this won't negatively affect any of our error messages though.

@alice-i-cecile
Copy link
Member

This is very implicit. There is a behavioral difference between .add_system(foo.run_if(X)).add_system(bar.run_if(X)) vs .add_systems((foo, bar).run_if(X)), which will be more pronounced the more "separate" foo and bar are in a schedule.

I'd like to consolidate this in the future, and make all instances of run conditions with the same name get checked exactly once. Users who want different timing can newtype. I think that the way that each run_if is evaluated seperately in some circumstances is already a footgun in confusing ways.

This makes "schedule visualization" more difficult as rendering these set names could be very unwieldy for large numbers of systems in a set.

Agreed, I'm feeling a bit iffy about the naming questions here.

As already mentioned, this has the potential to create a lot of "useless" sets. I think we should avoid creating these sets when they aren't needed (ex: they dont have before/after/run_if/in_set).

Agreed. Now that I'm thinking more about how this works, I think this should be fixed here to avoid making a complete mess of scheduling viz.

@github-actions
Copy link
Contributor

Example alien_cake_addict failed to run, please try running it locally and check the result.

1 similar comment
@github-actions
Copy link
Contributor

Example alien_cake_addict failed to run, please try running it locally and check the result.

@geieredgar
Copy link
Contributor Author

geieredgar commented Feb 15, 2023

As already mentioned, this has the potential to create a lot of "useless" sets. I think we should avoid creating these sets when they aren't needed (ex: they dont have before/after/run_if/in_set).

Agreed. Now that I'm thinking more about how this works, I think this should be fixed here to avoid making a complete mess of scheduling viz.

Okay, I adjusted add_systems to only create an anonymous system set if at least one run condition was added. This should limit the impact of this PR on scheduling viz as long as run_if is not used.

This makes "schedule visualization" more difficult as rendering these set names could be very unwieldy for large numbers of systems in a set.

Agreed, I'm feeling a bit iffy about the naming questions here.

I agree that for schedule visualizations detailed names aren't necessary because they are redundant if you can already see the members in the visualization.

  1. Idk if including the system names in the anonymous sets is ideal. We can already do "set membership" calculations elsewhere if we need this info, and any "set debug" view will already show the members of systems in a set. Additionally, it is a O(N) allocation that is only useful in a debug context. Probably better to just give anonymous sets ascending ids or something. Ex: AnonymousSet(5). We should double check that this won't negatively affect any of our error messages though.

I have switched to using an incremented atomic counter for the implicit sets. I already have thought about that, but was worried about the error messages. But now I know that we can simply reconstruct the detailed set name when an error occurs. But I would like to do this in an separate PR.

What do you think about adding an is_anonymous() method to the SystemSet trait? I want to do this to distinguish between using the Debug trait for the name and constructing the name from the set members for the error message code. But this might also be useful for the visualization to skip/unpack those sets.

@geieredgar
Copy link
Contributor Author

Summarization of my latest changes:

  • I've made sure that error messages for a call add_systems((a, b)) would print out the anonymous set as "(<name of a>, <name of b>)" instead of "AnonymousSet(<ID>)". To do that, I temporarily removed the Debug trait from SystemSet and looked where is was used.
  • Added is_anonymous() method to SystemSet to be able to distinguish between anonymous and non-anonymous system sets. The schedule visualization should use this information to directly apply all dependencies and relations from the system set to its members, to make the anonymous set invisible in the visualization.
  • Renamed AnonymousSystemSet to AnonymousSet, I like the shorter name better.
  • Anonymous sets are now always created except in the two simple cases of add_systems((a, b, ...)) and add_systems((a, b, ...).chain()). This made the implementation simpler, because we don't have to copy from graph_info of the wholeSystemConfigs into each SystemConfig. It also retains the benefits of a smaller schedule graph mentioned by cart.

With this, I think every mentioned concern excluding the behavioral difference between .add_system(foo.run_if(X)).add_system(bar.run_if(X)) and .add_systems((foo, bar).run_if(X)) is addressed.

Copy link
Contributor

@hymm hymm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should add some documentation to App::add_systems and Schedule::add_systems that using run_if, after, etc adds a anonymous system set.

crates/bevy_ecs/src/schedule/config.rs Outdated Show resolved Hide resolved
@cart
Copy link
Member

cart commented Feb 15, 2023

@alice-i-cecile:

I'd like to consolidate this in the future, and make all instances of run conditions with the same name get checked exactly once. Users who want different timing can newtype. I think that the way that each run_if is evaluated seperately in some circumstances is already a footgun in confusing ways.

Hmmm not (yet) sold on that. Having conditions be "assigned" to specific groups of systems, and having them run right before the first system in the group is definitely implicit. But it ties the condition to specific contexts / places in the schedule, which gives them a good chance of being evaluated at the "right time". And the behavior is predictable (and roughly controllable), if you know the rules.

Evaluating them globally removes a lot of "locational context". Definitely a hard problem / one worth discussing. Something we could consider: add "dataflow" to the graph to make these things more clear. Allow scheduling systems that produce outputs (ex: System<Out = bool>), which other systems can then depend on. Run conditions would then be configured to run just like any other system. We could have run_if(some_condition_system) behave more like after() edges. And if we accept arbitrary output types, we could enable more interesting dataflow.

Copy link
Member

@cart cart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latest changes resolve the concerns I listed. I'm on board!

crates/bevy_ecs/src/schedule/set.rs Outdated Show resolved Hide resolved
crates/bevy_ecs/src/schedule/schedule.rs Outdated Show resolved Hide resolved
@geieredgar
Copy link
Contributor Author

geieredgar commented Feb 15, 2023

I've commited all requested changes (except for the dyn_clone() part). Outside my remark about the rustdoc item link I think this is ready for further reviews / merging.

@maniwani
Copy link
Contributor

maniwani commented Feb 16, 2023

SystemConfigs was meant to be "distributive", with add_systems being equivalent to calling add_system on the individual tuple members. The chain method was an exception (but it only exists on the tuple trait anyway).

From a first pass, I do not like the change from "distributive" to "transitive".

This is very implicit. There is a behavioral difference between .add_system(foo.run_if(X)).add_system(bar.run_if(X)) vs .add_systems((foo, bar).run_if(X)), which will be more pronounced the more "separate" foo and bar are in a schedule.

Yeah, I think this behavior is too subtle. I'd be onboard if SystemConfigs remains "distributive" and either we (edit: just add a Clone bound or) pick a different method name (i.e. not run_if) or add an is_anonymous_set (bikeshed) that indicates "special treatment" like chain does.

  • It makes run conditions "more optimal" by only running them once for a group of systems.
  • It makes our graphs smaller and therefore faster to "crunch".

IMO these aren't pros. The act of putting things in a set can give benefits, but we do not want to group systems by accident.

I'd like to consolidate this in the future, and make all instances of run conditions with the same name get checked exactly once. Users who want different timing can newtype. I think that the way that each run_if is evaluated seperately in some circumstances is already a footgun in confusing ways.

This doesn't really make sense. Take run_if(in_state(X)) as an example. It should work anywhere in the schedule. IMO the current behavior is clear and intuitive, but I'm obviously biased since I wrote it.

  • conditions attached to a system run just before the system does
  • conditions attached to a system set run just before any of its systems do

(The "just before" is literal, but the actual invariant is that any data accessed by the conditions is not mutated in between the conditions being evaluated and the (first of the) guarded system(s) running.)

Run conditions would then be configured to run just like any other system. We could have run_if(some_condition_system) behave more like after() edges. And if we accept arbitrary output types, we could enable more interesting dataflow.

AFAIK dependencies can't (yet) express "before and after + no writer in between". That realization is what led to the current impl. It's the same problem as "before and after + sync point in between". Even if we could do those, having conditions in the graph would lengthen schedule build times (and make visualizations noiser unless they were filtered out).

Allow scheduling systems that produce outputs (ex: System<Out = bool>)

systems as entities

@geieredgar
Copy link
Contributor Author

geieredgar commented Feb 16, 2023

@maniwani How about adding a as_set() method that sets a marker field like chain() and documenting that this is currently required for using run_if, enforcing it via panic at runtime, if the marker is not set.
This gives users the ability to choose and removes the implicitness.

This is probably what you meant by is_anonymous_set, right?

@maniwani
Copy link
Contributor

maniwani commented Feb 16, 2023

Yep, I would be satisfied with that.

Though we still wouldn't have a method that does copy a condition to all systems like a hypothetical run_if<P, C: Condition<P> + Clone>(self, condition: C) would allow. But that's fine with me.

@geieredgar
Copy link
Contributor Author

I extracted the distributive_run_if part to a new PR: #7724, because it does not require the controversial addition of anonymous system sets.

@alice-i-cecile alice-i-cecile added the X-Controversial There is active debate or serious implications around merging this PR label Feb 17, 2023
bors bot pushed a commit that referenced this pull request Feb 18, 2023
# Objective

- Fixes #7659.

## Solution

- This PR extracted the `distributive_run_if` part of #7676, because it does not require the controversial introduction of anonymous system sets.
- The distinctive name should make the user aware about the differences between `IntoSystemConfig::run_if` and `IntoSystemConfigs::distributive_run_if`.
- The documentation explains in detail the consequences of using the API and possible pit falls when using it.
- A test demonstrates the possibility of changing the condition result, resulting in some of the systems not being run.

---

## Changelog

### Added
- Add `distributive_run_if` to `IntoSystemConfigs` to enable adding a run condition to each system when using `add_systems`.
myreprise1 pushed a commit to myreprise1/bevy that referenced this pull request Feb 20, 2023
# Objective

- Fixes bevyengine#7659.

## Solution

- This PR extracted the `distributive_run_if` part of bevyengine#7676, because it does not require the controversial introduction of anonymous system sets.
- The distinctive name should make the user aware about the differences between `IntoSystemConfig::run_if` and `IntoSystemConfigs::distributive_run_if`.
- The documentation explains in detail the consequences of using the API and possible pit falls when using it.
- A test demonstrates the possibility of changing the condition result, resulting in some of the systems not being run.

---

## Changelog

### Added
- Add `distributive_run_if` to `IntoSystemConfigs` to enable adding a run condition to each system when using `add_systems`.
@marstaik
Copy link

marstaik commented Mar 29, 2023

Looking forward to this.
I'm new to bevy and by assumption (in regards to how the other commands work ergonomically) I expected .add_systems((a,b,c).run_if(...))) to work, and was surprised today to find it didn't already work this way.

Following the discussions I thought I'd drop another idea

// Regular
pub fn add_system<M>(&mut self, system: impl IntoSystemAppConfig<M>)

// Applies all .run_if(...), .before(...), .after(...) in a distributive fashion to all systems, so basically clone the conditions to every system
pub fn add_systems<M>(&mut self, systems: impl IntoSystemAppConfigs<M>)

// Create an anonymous set (sys_a, sys_b) such that run_if(...), before(...), .after(...) applies to the anonymous set
pub fn add_system_set<M>(&mut self, systems: impl IntoSystemSetAppConfig<M>)

Then I see some weirdness with .chain() as being the outlier, which could potentially be replaced with an ordered() system set, like so:

app.add_systems((a,b,c).chain())

// vs

app.add_system_set((a,b,c).ordered())

It seems like a good example of future cases where there might be markers such as ordered that would be set-specific instead of system specific.

P.S.

I found .in_set(OnUpdate(AppState::XXX)) really confusing instead of applying a blanket .run_if()

@alice-i-cecile alice-i-cecile added this to the 0.11 milestone Mar 30, 2023
@alice-i-cecile alice-i-cecile added the S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it label Mar 30, 2023
@alice-i-cecile
Copy link
Member

@geieredgar, can you resolve the conflicts or rebase? Once that's done, this should be good to merge.

@geieredgar
Copy link
Contributor Author

I've rebased the PR, but I am not sure if it would be better to just change run_if instead of adding collective_run_if.

@alice-i-cecile
Copy link
Member

I've rebased the PR, but I am not sure if it would be better to just change run_if instead of adding collective_run_if.

Agreed, I think this is the way forward (cc @cart @maniwani). We may also want to remove distributive_run_if, but that should be completed and discussed seperately IMO.

Copy link
Member

@cart cart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is the right general approach, although the implementation doesn't actually work atm (see comments).

I have fixes prepared that I'll push now.

crates/bevy_ecs/src/schedule/schedule.rs Outdated Show resolved Hide resolved
crates/bevy_ecs/src/schedule/schedule.rs Outdated Show resolved Hide resolved
crates/bevy_ecs/src/schedule/schedule.rs Outdated Show resolved Hide resolved
@cart cart enabled auto-merge March 30, 2023 21:26
@cart cart added this pull request to the merge queue Mar 30, 2023
Merged via the queue into bevyengine:main with commit 300b275 Mar 30, 2023
github-merge-queue bot pushed a commit that referenced this pull request Oct 25, 2023
# Objective

First of all, this PR took heavy inspiration from #7760 and #5715. It
intends to also fix #5569, but with a slightly different approach.


This also fixes #9335 by reexporting `DynEq`.

## Solution

The advantage of this API is that we can intern a value without
allocating for zero-sized-types and for enum variants that have no
fields. This PR does this automatically in the `SystemSet` and
`ScheduleLabel` derive macros for unit structs and fieldless enum
variants. So this should cover many internal and external use cases of
`SystemSet` and `ScheduleLabel`. In these optimal use cases, no memory
will be allocated.

- The interning returns a `Interned<dyn SystemSet>`, which is just a
wrapper around a `&'static dyn SystemSet`.
- `Hash` and `Eq` are implemented in terms of the pointer value of the
reference, similar to my first approach of anonymous system sets in
#7676.
- Therefore, `Interned<T>` does not implement `Borrow<T>`, only `Deref`.
- The debug output of `Interned<T>` is the same as the interned value.

Edit: 
- `AppLabel` is now also interned and the old
`derive_label`/`define_label` macros were replaced with the new
interning implementation.
- Anonymous set ids are reused for different `Schedule`s, reducing the
amount of leaked memory.

### Pros
- `InternedSystemSet` and `InternedScheduleLabel` behave very similar to
the current `BoxedSystemSet` and `BoxedScheduleLabel`, but can be copied
without an allocation.
- Many use cases don't allocate at all.
- Very fast lookups and comparisons when using `InternedSystemSet` and
`InternedScheduleLabel`.
- The `intern` module might be usable in other areas.
- `Interned{ScheduleLabel, SystemSet, AppLabel}` does implement
`{ScheduleLabel, SystemSet, AppLabel}`, increasing ergonomics.

### Cons
- Implementors of `SystemSet` and `ScheduleLabel` still need to
implement `Hash` and `Eq` (and `Clone`) for it to work.

## Changelog

### Added

- Added `intern` module to `bevy_utils`.
- Added reexports of `DynEq` to `bevy_ecs` and `bevy_app`.

### Changed

- Replaced `BoxedSystemSet` and `BoxedScheduleLabel` with
`InternedSystemSet` and `InternedScheduleLabel`.
- Replaced `impl AsRef<dyn ScheduleLabel>` with `impl ScheduleLabel`.
- Replaced `AppLabelId` with `InternedAppLabel`.
- Changed `AppLabel` to use `Debug` for error messages.
- Changed `AppLabel` to use interning.
- Changed `define_label`/`derive_label` to use interning. 
- Replaced `define_boxed_label`/`derive_boxed_label` with
`define_label`/`derive_label`.
- Changed anonymous set ids to be only unique inside a schedule, not
globally.
- Made interned label types implement their label trait. 

### Removed

- Removed `define_boxed_label` and `derive_boxed_label`. 

## Migration guide

- Replace `BoxedScheduleLabel` and `Box<dyn ScheduleLabel>` with
`InternedScheduleLabel` or `Interned<dyn ScheduleLabel>`.
- Replace `BoxedSystemSet` and `Box<dyn SystemSet>` with
`InternedSystemSet` or `Interned<dyn SystemSet>`.
- Replace `AppLabelId` with `InternedAppLabel` or `Interned<dyn
AppLabel>`.
- Types manually implementing `ScheduleLabel`, `AppLabel` or `SystemSet`
need to implement:
  - `dyn_hash` directly instead of implementing `DynHash`
  - `as_dyn_eq`
- Pass labels to `World::try_schedule_scope`, `World::schedule_scope`,
`World::try_run_schedule`. `World::run_schedule`, `Schedules::remove`,
`Schedules::remove_entry`, `Schedules::contains`, `Schedules::get` and
`Schedules::get_mut` by value instead of by reference.

---------

Co-authored-by: Joseph <21144246+JoJoJet@users.noreply.github.com>
Co-authored-by: Carter Anderson <mcanders1@gmail.com>
ameknite pushed a commit to ameknite/bevy that referenced this pull request Nov 6, 2023
# Objective

First of all, this PR took heavy inspiration from bevyengine#7760 and bevyengine#5715. It
intends to also fix bevyengine#5569, but with a slightly different approach.


This also fixes bevyengine#9335 by reexporting `DynEq`.

## Solution

The advantage of this API is that we can intern a value without
allocating for zero-sized-types and for enum variants that have no
fields. This PR does this automatically in the `SystemSet` and
`ScheduleLabel` derive macros for unit structs and fieldless enum
variants. So this should cover many internal and external use cases of
`SystemSet` and `ScheduleLabel`. In these optimal use cases, no memory
will be allocated.

- The interning returns a `Interned<dyn SystemSet>`, which is just a
wrapper around a `&'static dyn SystemSet`.
- `Hash` and `Eq` are implemented in terms of the pointer value of the
reference, similar to my first approach of anonymous system sets in
bevyengine#7676.
- Therefore, `Interned<T>` does not implement `Borrow<T>`, only `Deref`.
- The debug output of `Interned<T>` is the same as the interned value.

Edit: 
- `AppLabel` is now also interned and the old
`derive_label`/`define_label` macros were replaced with the new
interning implementation.
- Anonymous set ids are reused for different `Schedule`s, reducing the
amount of leaked memory.

### Pros
- `InternedSystemSet` and `InternedScheduleLabel` behave very similar to
the current `BoxedSystemSet` and `BoxedScheduleLabel`, but can be copied
without an allocation.
- Many use cases don't allocate at all.
- Very fast lookups and comparisons when using `InternedSystemSet` and
`InternedScheduleLabel`.
- The `intern` module might be usable in other areas.
- `Interned{ScheduleLabel, SystemSet, AppLabel}` does implement
`{ScheduleLabel, SystemSet, AppLabel}`, increasing ergonomics.

### Cons
- Implementors of `SystemSet` and `ScheduleLabel` still need to
implement `Hash` and `Eq` (and `Clone`) for it to work.

## Changelog

### Added

- Added `intern` module to `bevy_utils`.
- Added reexports of `DynEq` to `bevy_ecs` and `bevy_app`.

### Changed

- Replaced `BoxedSystemSet` and `BoxedScheduleLabel` with
`InternedSystemSet` and `InternedScheduleLabel`.
- Replaced `impl AsRef<dyn ScheduleLabel>` with `impl ScheduleLabel`.
- Replaced `AppLabelId` with `InternedAppLabel`.
- Changed `AppLabel` to use `Debug` for error messages.
- Changed `AppLabel` to use interning.
- Changed `define_label`/`derive_label` to use interning. 
- Replaced `define_boxed_label`/`derive_boxed_label` with
`define_label`/`derive_label`.
- Changed anonymous set ids to be only unique inside a schedule, not
globally.
- Made interned label types implement their label trait. 

### Removed

- Removed `define_boxed_label` and `derive_boxed_label`. 

## Migration guide

- Replace `BoxedScheduleLabel` and `Box<dyn ScheduleLabel>` with
`InternedScheduleLabel` or `Interned<dyn ScheduleLabel>`.
- Replace `BoxedSystemSet` and `Box<dyn SystemSet>` with
`InternedSystemSet` or `Interned<dyn SystemSet>`.
- Replace `AppLabelId` with `InternedAppLabel` or `Interned<dyn
AppLabel>`.
- Types manually implementing `ScheduleLabel`, `AppLabel` or `SystemSet`
need to implement:
  - `dyn_hash` directly instead of implementing `DynHash`
  - `as_dyn_eq`
- Pass labels to `World::try_schedule_scope`, `World::schedule_scope`,
`World::try_run_schedule`. `World::run_schedule`, `Schedules::remove`,
`Schedules::remove_entry`, `Schedules::contains`, `Schedules::get` and
`Schedules::get_mut` by value instead of by reference.

---------

Co-authored-by: Joseph <21144246+JoJoJet@users.noreply.github.com>
Co-authored-by: Carter Anderson <mcanders1@gmail.com>
rdrpenguin04 pushed a commit to rdrpenguin04/bevy that referenced this pull request Jan 9, 2024
# Objective

First of all, this PR took heavy inspiration from bevyengine#7760 and bevyengine#5715. It
intends to also fix bevyengine#5569, but with a slightly different approach.


This also fixes bevyengine#9335 by reexporting `DynEq`.

## Solution

The advantage of this API is that we can intern a value without
allocating for zero-sized-types and for enum variants that have no
fields. This PR does this automatically in the `SystemSet` and
`ScheduleLabel` derive macros for unit structs and fieldless enum
variants. So this should cover many internal and external use cases of
`SystemSet` and `ScheduleLabel`. In these optimal use cases, no memory
will be allocated.

- The interning returns a `Interned<dyn SystemSet>`, which is just a
wrapper around a `&'static dyn SystemSet`.
- `Hash` and `Eq` are implemented in terms of the pointer value of the
reference, similar to my first approach of anonymous system sets in
bevyengine#7676.
- Therefore, `Interned<T>` does not implement `Borrow<T>`, only `Deref`.
- The debug output of `Interned<T>` is the same as the interned value.

Edit: 
- `AppLabel` is now also interned and the old
`derive_label`/`define_label` macros were replaced with the new
interning implementation.
- Anonymous set ids are reused for different `Schedule`s, reducing the
amount of leaked memory.

### Pros
- `InternedSystemSet` and `InternedScheduleLabel` behave very similar to
the current `BoxedSystemSet` and `BoxedScheduleLabel`, but can be copied
without an allocation.
- Many use cases don't allocate at all.
- Very fast lookups and comparisons when using `InternedSystemSet` and
`InternedScheduleLabel`.
- The `intern` module might be usable in other areas.
- `Interned{ScheduleLabel, SystemSet, AppLabel}` does implement
`{ScheduleLabel, SystemSet, AppLabel}`, increasing ergonomics.

### Cons
- Implementors of `SystemSet` and `ScheduleLabel` still need to
implement `Hash` and `Eq` (and `Clone`) for it to work.

## Changelog

### Added

- Added `intern` module to `bevy_utils`.
- Added reexports of `DynEq` to `bevy_ecs` and `bevy_app`.

### Changed

- Replaced `BoxedSystemSet` and `BoxedScheduleLabel` with
`InternedSystemSet` and `InternedScheduleLabel`.
- Replaced `impl AsRef<dyn ScheduleLabel>` with `impl ScheduleLabel`.
- Replaced `AppLabelId` with `InternedAppLabel`.
- Changed `AppLabel` to use `Debug` for error messages.
- Changed `AppLabel` to use interning.
- Changed `define_label`/`derive_label` to use interning. 
- Replaced `define_boxed_label`/`derive_boxed_label` with
`define_label`/`derive_label`.
- Changed anonymous set ids to be only unique inside a schedule, not
globally.
- Made interned label types implement their label trait. 

### Removed

- Removed `define_boxed_label` and `derive_boxed_label`. 

## Migration guide

- Replace `BoxedScheduleLabel` and `Box<dyn ScheduleLabel>` with
`InternedScheduleLabel` or `Interned<dyn ScheduleLabel>`.
- Replace `BoxedSystemSet` and `Box<dyn SystemSet>` with
`InternedSystemSet` or `Interned<dyn SystemSet>`.
- Replace `AppLabelId` with `InternedAppLabel` or `Interned<dyn
AppLabel>`.
- Types manually implementing `ScheduleLabel`, `AppLabel` or `SystemSet`
need to implement:
  - `dyn_hash` directly instead of implementing `DynHash`
  - `as_dyn_eq`
- Pass labels to `World::try_schedule_scope`, `World::schedule_scope`,
`World::try_run_schedule`. `World::run_schedule`, `Schedules::remove`,
`Schedules::remove_entry`, `Schedules::contains`, `Schedules::get` and
`Schedules::get_mut` by value instead of by reference.

---------

Co-authored-by: Joseph <21144246+JoJoJet@users.noreply.github.com>
Co-authored-by: Carter Anderson <mcanders1@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events C-Usability A targeted quality-of-life change that makes Bevy easier to use D-Complex Quite challenging from either a design or technical perspective. Ask for help! S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Controversial There is active debate or serious implications around merging this PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support run_if for SystemConfigs
6 participants