-
-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Add 'World::run_system_with_input' function + allow World::run_system to get system output
#10380
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
Add 'World::run_system_with_input' function + allow World::run_system to get system output
#10380
Conversation
|
Welcome, new contributor! Please make sure you've read our contributing guide and we look forward to reviewing your pull request shortly ✨ |
ea11dd5 to
7728b9f
Compare
|
Inputs are not included in oneshots because if your system has no input you need to write |
World::run_system to get system output
|
Supporting output is easy enough and churns more-or-less the same API surface, so I've added this to the PR. |
| #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] | ||
| pub struct SystemId(Entity); | ||
| #[derive(Eq)] | ||
| pub struct SystemId<I = (), O = ()>(Entity, std::marker::PhantomData<fn(I) -> O>); |
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.
The particular type in the PhantomData doesn't really matter as long as it mentions both I and O, so I figured it might as well be fn(I) -> O since that (mnemonically) describes how the parameters are used.
6f2e243 to
e711294
Compare
|
This looks good and I think it's a great expansion. I'll have to do a line-by-line review later, but for now, I'd like to point out that Also, can you now register and run |
This is a good question - I'm not sure. I'll check later whether it's possible. If so, it should be documented and tested. |
|
I haven't reviewed this closely yet, but is adding generics to (edit: added paragraph) |
| /// A small wrapper for [`BoxedSystem`] that also keeps track whether or not the system has been initialized. | ||
| #[derive(Component)] | ||
| struct RegisteredSystem { | ||
| struct RegisteredSystem<I, O> { |
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 may significantly increase the number of registered components within a World. Every new input-output signature registered in this way will result in a new Component type and ComponentId being allocated.
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.
Agreed, I would really like to type-erase this.
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 see how inputs and outputs can be done without type information.
What is the harm in having a lot of registered components?
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.
Hmm okay, I see your point.
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 started looking at how this might be done- by boxing the system again, we can store a Box<dyn Any + 'static + Send + Sync> whose contents are the BoxedSystem<I, O>.
By keeping SystemId<I, O> generic, we know what to downcast to inside of the functions that access the RegisteredSystem. So it remains possible, at the cost of 1 extra box allocation per registered system.
I think that, to maintain the current API and generally make run_system{_with_input} ergonomic, in this case the downcast would be assumed to succeed, using .expect(...) - there are no reasonable strategies to handle a downcasting error. Since the API doesn't expose RegisteredSystem we can assume that with any reasonable use, the types will match at runtime, using the API border here to keep them matched up.
On the other hand, I don't really see how many types of registered components would affect performance - it's still user-bounded by how many different one-shot systems the user creates, in terms of which types are used.
This is also an internal API, so if there is a need to refactor later for performance reasons, it can be done with little user-facing impact. If this is currently "fast enough" I'd rather get extra power into developers hands sooner and then if there are specific cases where it is too slow can go back and try to benchmark and improve.
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.
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, at least for me, I'm fine if this is merged with the existing architecture. Like you said, it's not public, this is incredibly useful functionality, and I don't think it's likely to be a serious performance footgun.
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.
What is the harm in having a lot of registered components?
I haven't measured it, but bloating the components metadata stores may have a negative impact on TypeId -> ComponentId lookups, which most prominently show up in the typed World::get and Entity{Ref/Mut}::get APIs, which may cause Command slowdowns. This previously wasn't too big of a concern given that the component set typically doesn't grow organically over time after initialization, or if it does, it's not reliant on the generic APIs (i.e. the *_by_id untyped APIs), but these changes might allow that to happen over the lifetime of a World.
I'll be a bit more clear and say that I don't think this is blocking. If we're treating SystemId as a temporary holdover, then this shouldn't be a huge issue, and I eventually want to find alternatives to that HashMap<TypeId, ComponentId> 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.
FYI another way this could be done would be adding a supertrait to System that's free of the In and Out associated types, and whose run_erased function takes two &mut dyn Any:
- the first one is supposed to point to a
Option<I>and will be.take()n by therun_erasedfunction to get its input; - the second one is supposed to point to a
Option<O>and will be filled by therun_erasedfunction with its output.
Then run_erased internally knows what the I and O types were supposed to be and can do the needed downcasts. This way any additional boxing is avoided, and only a dynamic check that the input and output types match up is performed when downcasting the &mut dyn Anys.
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.
Some extra things worth adding, in my opinion:
- Adapt the RunSystem command to become RunSystemWithInput
- Clearly document that the command version does not return any output
Other than that, I am not particularly worried about SystemId getting type parameters. I think the number of components will stay relatively small as most one-shot systems will have no input nor output, and because every component is tied to at least one system, you'd have to have a lot of systems for it to become a problem. I just don't think it's likely and as discussed, SystemId is not permanent.
It's worth looking into how this would work without type parameters but given the current design, this is a great extension. At the end of the day, I really like this API and I consider the existence of SystemId to be more of an implementation detail.
e711294 to
6346ccd
Compare
6982a45 to
7f68e79
Compare
f1cb295 to
90eaaa9
Compare
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.
It seems that the CI still has some complaints, but once those are fixed, I think this PR is ready to go.
Thank you for contributing!
|
Should be all fixed up now! 🚢 Thanks for reviewing! |
UkoeHB
left a comment
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.
Looks clean
|
I'm happy with the final API, the docs and tests are great, and this functionality is very useful. We can refactor internals later if there are strong proposals (or demonstrated problems). |
…m` to get system output (bevyengine#10380) # Objective Allows chained systems taking an `In<_>` input parameter to be run as one-shot systems. This API was mentioned in bevyengine#8963. In addition, `run_system(_with_input)` returns the system output, for any `'static` output type. ## Solution A new function, `World::run_system_with_input` allows a `SystemId<I, O>` to be run by providing an `I` value as input and producing `O` as an output. `SystemId<I, O>` is now generic over the input type `I` and output type `O`, along with the related functions and types `RegisteredSystem`, `RemovedSystem`, `register_system`, `remove_system`, and `RegisteredSystemError`. These default to `()`, preserving the existing API, for all of the public types. --- ## Changelog - Added `World::run_system_with_input` function to allow one-shot systems that take `In<_>` input parameters - Changed `World::run_system` and `World::register_system` to support systems with return types beyond `()` - Added `Commands::run_system_with_input` command that schedules a one-shot system with an `In<_>` input parameter
Objective
Allows chained systems taking an
In<_>input parameter to be run as one-shot systems. This API was mentioned in #8963.In addition,
run_system(_with_input)returns the system output, for any'staticoutput type.Solution
A new function,
World::run_system_with_inputallows aSystemId<I, O>to be run by providing anIvalue as input and producingOas an output.SystemId<I, O>is now generic over the input typeIand output typeO, along with the related functions and typesRegisteredSystem,RemovedSystem,register_system,remove_system, andRegisteredSystemError. These default to(), preserving the existing API, for all of the public types.Changelog
World::run_system_with_inputfunction to allow one-shot systems that takeIn<_>input parametersWorld::run_systemandWorld::register_systemto support systems with return types beyond()Commands::run_system_with_inputcommand that schedules a one-shot system with anIn<_>input parameter