-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
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) |
@@ -7,18 +7,18 @@ use thiserror::Error; | |||
|
|||
/// 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_erased
function to get its input; - the second one is supposed to point to a
Option<O>
and will be filled by therun_erased
function 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 Any
s.
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! |
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'static
output type.Solution
A new function,
World::run_system_with_input
allows aSystemId<I, O>
to be run by providing anI
value as input and producingO
as an output.SystemId<I, O>
is now generic over the input typeI
and 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_input
function to allow one-shot systems that takeIn<_>
input parametersWorld::run_system
andWorld::register_system
to support systems with return types beyond()
Commands::run_system_with_input
command that schedules a one-shot system with anIn<_>
input parameter