-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
Fast and correct one-shot systems with a system registry resource #4090
Conversation
} else { | ||
self.register_system(world, system); | ||
self.run_system_by_type_id(world, type_id); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a good reason why an unregistered system should be allowed (through implicit registration) to run?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes: it makes the API dramatically simpler to use.
With implicit registration, the user can call world.run_system(my_system)
on an ad-hoc basis anywhere in their code, which is fantastic for prototyping and scripting.
Without it, they have to remember to register it, and then remove that registration when the code changes.
Manual registration is only there to enable TypeId
driven workflows. These are much easier to store and work with than an instance of the function, but I would expect manual registration to be hidden in external user-facing code as well :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My original argument against this was that users could accidentally run arbitrary systems, which would lead to weird behavior, but the ease of use definitely beats that.
On the second thought, centralizing which functions can be run ad hoc would be helpful for the editor in far future.
Going with that idea, maybe throwing warnings would be a decent middle ground? That way users can still prototype easily, but registration would be enforced.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the warnings are helpful enough to warrant the level of noise. I'm not sure we're protecting against a real class of errors: this isn't any different than just adding systems to a schedule.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah but all schedule systems are known at app launch,
from what I understand the one-shot systems can be registered at any point in runtime.
So in editors case it won't be aware of this until it is ran at least once, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah but all schedule systems are known at app launch,
This is only true for now. See #279. I suppose we could opt-in to warnings, but IMO that sort of design is much more easily explored once we have a way to display all of the systems in the schedule.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does seem like a reasonable design. Some assorted thoughts:
- When I pitched my idea for this, it involved "run by label", as that is how we encourage people to refer to systems. Encouraging users to think in "type ids" is a "new" and inconsistent way to reference systems. Given that (as of today) we now auto-label system functions, using actual labels would be better i think (and equally ergonomic).
- I think we should consider embracing the "one label to many systems" design, as that maps best to the label mental model and enables new and interesting call patterns (ex: "run all event handler systems now"). It would require decoupling "registering systems" from "running systems", but imo this would be an improvement. The current implicit "overwrite systems with the same type id" approach feels a bit footgunny. And "pre-defining" systems is already how we do things.
Ultimately I think this pattern could also be embraced by the scheduler (ex: expose a way to queue a run-by-label for the current schedule, using the schedule as the source of systems instead of a SystemRegistry resource ... or merging the two concepts). SystemRegistry is really just a very simple schedule impl. In a "running many labels" scenario, it would be cool to (optionally) do that using "parallel system scheduling".
Agreed :) This was not the case when I made this PR, but I'm happy to swap over.
Yep, this lines up very nicely with some of the ideas in Stageless about "subgraphs" and "multiple schedules".
Yep, this was @maniwani's thinking too: eventually I'd like to converge this. |
cbd2243
to
2ab12f7
Compare
No plugin exists for bevy_ecs, so this was the best place.
Only the commands specific to this system are applied, so we should de-emphasize this choice.
Co-authored-by: Aevyrie <[email protected]>
Co-authored-by: Federico Rinaldi <[email protected]>
Co-authored-by: Aevyrie <[email protected]>
Sorry for the late response. I can describe the setup I have in #4391.
I basically came to the same conclusions as cart. This registry data structure is mentioned in the RFC and included in #4391. The "one label to many systems" design is exactly what system sets have become. I've taken to just calling it These are (latest versions of) the important types. pub struct Systems {
// used for lookup
index: HashMap<SystemLabelId, NodeId>,
next_id: u64,
// main storage
systems: HashMap<NodeId, Option<BoxedSystem>>,
conditions: HashMap<NodeId, Option<Vec<BoxedRunCondition>>>,
schedules: HashMap<NodeId, Option<Schedule>>,
graphs: HashMap<NodeId, DependencyGraph>,
// needed for graph updates
hierarchy: DiGraph<NodeId>,
uninit_nodes: Vec<NodeInfo>,
}
pub enum NodeId {
System(u64),
Set(u64),
}
struct DependencyGraph {
/// dependency graph
base: DiGraph<NodeId>,
/// flattened version of the dependency graph (only system nodes and system-system edges)
flat: DiGraph<NodeId>,
/// cached topological ordering of `flat`
topsort: Vec<NodeId>,
/// `Graph` if the graph (and schedule) need to be rebuilt, `Schedule` if just the schedule, `None` otherwise
rebuild: Rebuild,
}
pub struct Schedule {
// All elements are sorted in topological order.
// NOTE: Might change all the `RefCell` to `Cell<Option<T>>`.
systems: Vec<RefCell<BoxedSystem>>,
system_conditions: Vec<RefCell<Vec<BoxedRunCondition>>>,
set_conditions: Vec<RefCell<Vec<BoxedRunCondition>>>,
system_ids: Vec<NodeId>,
set_ids: Vec<NodeId>,
system_deps: Vec<(usize, Vec<usize>)>,
sets_of_systems: Vec<FixedBitSet>,
systems_of_sets: Vec<FixedBitSet>,
} Summarizing:
So basically, you only incur the costs of schedules you use, there's no ownership dilemma, and any (Edit: Also, just want to highlight how |
FYI if anyone wants to pick this up while I'm on vacation this month feel free! Otherwise I'll wrap this up in September. |
I think we should close this out now that stageless / #6587 is about to land. Running systems on demand will be possible via Schedules: app
.add_system_to(OnDemand, foo)
.add_system(bar);
fn bar(world: &mut World) {
world.run_schedule(OnDemand);
} |
(FYI I'm not trying to shutting the conversation down. Happy to open this back up if anyone wants. Stageless doesn't currently include "run systems from commands", but I think that impl would be different (and much smaller) in the new world). |
Also one of the concerns in this PR was that systemstate isn't cacheable, but it is now! fn bar(world: &mut World, state: &mut SystemState<Res<Time>>) {
let delta = state.get(world).delta_seconds();
if delta > 2.0 {
world.run_schedule(OnDemand);
}
} |
Yep! I agree with this decision! |
I'm adopting this ~~child~~ PR. # Objective - Working with exclusive world access is not always easy: in many cases, a standard system or three is more ergonomic to write, and more modularly maintainable. - For small, one-off tasks (commonly handled with scripting), running an event-reader system incurs a small but flat overhead cost and muddies the schedule. - Certain forms of logic (e.g. turn-based games) want very fine-grained linear and/or branching control over logic. - SystemState is not automatically cached, and so performance can suffer and change detection breaks. - Fixes #2192. - Partial workaround for #279. ## Solution - Adds a SystemRegistry resource to the World, which stores initialized systems keyed by their SystemSet. - Allows users to call world.run_system(my_system) and commands.run_system(my_system), without re-initializing or losing state (essential for change detection). - Add a Callback type to enable convenient use of dynamic one shot systems and reduce the mental overhead of working with Box<dyn SystemSet>. - Allow users to run systems based on their SystemSet, enabling more complex user-made abstractions. ## Future work - Parameterized one-shot systems would improve reusability and bring them closer to events and commands. The API could be something like run_system_with_input(my_system, my_input) and use the In SystemParam. - We should evaluate the unification of commands and one-shot systems since they are two different ways to run logic on demand over a World. ### Prior attempts - #2234 - #2417 - #4090 - #7999 This PR continues the work done in #7999. --------- Co-authored-by: Alice Cecile <[email protected]> Co-authored-by: Federico Rinaldi <[email protected]> Co-authored-by: MinerSebas <[email protected]> Co-authored-by: Aevyrie <[email protected]> Co-authored-by: Alejandro Pascual Pozo <[email protected]> Co-authored-by: Rob Parrett <[email protected]> Co-authored-by: François <[email protected]> Co-authored-by: Dmytro Banin <[email protected]> Co-authored-by: James Liu <[email protected]>
I'm adopting this ~~child~~ PR. # Objective - Working with exclusive world access is not always easy: in many cases, a standard system or three is more ergonomic to write, and more modularly maintainable. - For small, one-off tasks (commonly handled with scripting), running an event-reader system incurs a small but flat overhead cost and muddies the schedule. - Certain forms of logic (e.g. turn-based games) want very fine-grained linear and/or branching control over logic. - SystemState is not automatically cached, and so performance can suffer and change detection breaks. - Fixes bevyengine#2192. - Partial workaround for bevyengine#279. ## Solution - Adds a SystemRegistry resource to the World, which stores initialized systems keyed by their SystemSet. - Allows users to call world.run_system(my_system) and commands.run_system(my_system), without re-initializing or losing state (essential for change detection). - Add a Callback type to enable convenient use of dynamic one shot systems and reduce the mental overhead of working with Box<dyn SystemSet>. - Allow users to run systems based on their SystemSet, enabling more complex user-made abstractions. ## Future work - Parameterized one-shot systems would improve reusability and bring them closer to events and commands. The API could be something like run_system_with_input(my_system, my_input) and use the In SystemParam. - We should evaluate the unification of commands and one-shot systems since they are two different ways to run logic on demand over a World. ### Prior attempts - bevyengine#2234 - bevyengine#2417 - bevyengine#4090 - bevyengine#7999 This PR continues the work done in bevyengine#7999. --------- Co-authored-by: Alice Cecile <[email protected]> Co-authored-by: Federico Rinaldi <[email protected]> Co-authored-by: MinerSebas <[email protected]> Co-authored-by: Aevyrie <[email protected]> Co-authored-by: Alejandro Pascual Pozo <[email protected]> Co-authored-by: Rob Parrett <[email protected]> Co-authored-by: François <[email protected]> Co-authored-by: Dmytro Banin <[email protected]> Co-authored-by: James Liu <[email protected]>
Objective
SystemState
is not automatically cached, and so performance can suffer and change detection breaks.Solution
SystemRegistry
resource to theWorld
, which stores initialized systems keyed by theirTypeId
.world.run_system(my_system)
andcommands.run_system(my_system)
, without re-initializing or losing state (essential for change detection).TypeId
, enabling more complex user-made abstractionsCallback
type to enable convenient use of dynamic one shot systems and reduce the mental overhead of working withBox<dyn SystemLabel>
.Status
System
responsible for updating its own archetypes #4115.Future work
&mut World
as a system param and make.exclusive_system()
optional #4166 is merged.run_system
usage, likely by movingSystemRegistry
into its own field onWorld
.Prior attempts
&mut World
#2417