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

Support more kinds of system params in buildable systems. #14050

Merged
merged 3 commits into from
Aug 12, 2024

Conversation

chescock
Copy link
Contributor

Objective

Support more kinds of system params in buildable systems, such as a ParamSet or Vec containing buildable params or tuples of buildable params.

Solution

Replace the BuildableSystemParam trait with SystemParamBuilder to make it easier to compose builders. Provide implementations for existing buildable params, plus tuples, ParamSet, and Vec.

Examples

// ParamSet of tuple: 
let system = (ParamSetBuilder((
    QueryParamBuilder::new(|builder| { builder.with::<B>(); }),
    QueryParamBuilder::new(|builder| { builder.with::<C>(); }),
)),)
    .build_state(&mut world)
    .build_system(|mut params: ParamSet<(Query<&mut A>, Query<&mut A>)>| {
        params.p0().iter().count() + params.p1().iter().count()
    });
	
// ParamSet of Vec:
let system = (ParamSetBuilder(vec![
    QueryParamBuilder::new_box(|builder| { builder.with::<B>(); }),
    QueryParamBuilder::new_box(|builder| { builder.with::<C>(); }),
]),)
    .build_state(&mut world)
    .build_system(|mut params: ParamSet<Vec<Query<&mut A>>>| {
        let mut count = 0;
        params.for_each(|mut query| count += query.iter_mut().count());
        count
    });

Migration Guide

The API for SystemBuilder has changed. Instead of constructing a builder with a world and then adding params, you first create a tuple of param builders and then supply the world.

// Before
let system = SystemBuilder::<()>::new(&mut world)
    .local::<u64>()
    .builder::<Local<u64>>(|x| *x = 10)
    .builder::<Query<&A>>(|builder| { builder.with::<B>(); })
    .build(system);

// After
let system = (
    ParamBuilder,
    LocalBuilder(10),
    QueryParamBuilder::new(|builder| { builder.with::<B>(); }),
)
    .build_state(&mut world)
    .build_system(system);

Possible Future Work

Here are a few possible follow-up changes. I coded them up to prove that this API can support them, but they aren't necessary for this PR.

@chescock chescock force-pushed the SystemParamBuilder branch from bfaef59 to 20577c8 Compare June 27, 2024 14:05
@alice-i-cecile alice-i-cecile added C-Feature A new feature, making something new possible A-ECS Entities, components, systems, and events S-Needs-Review Needs reviewer attention (from anyone!) to move forward M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide labels Jun 27, 2024
@alice-i-cecile alice-i-cecile added this to the 0.15 milestone Jun 27, 2024
@ScottKane
Copy link

It would be nice if this API could support some of the other SystemParam's, right now I'm working on bindings to bevy using the existing API. As it stands I can create components and systems from C# and they work, however there is currently no way to query for Res/ResMut even though they can be inserted on the world by component id. Would also be nice if this supported Commands, I think this is what this feature needs to really help with other languages working with bevy.

@ScottKane
Copy link

ScottKane commented Jul 12, 2024

In case it's useful to see some of the things I'm using this API to do, currently this will only support one Query per system because doing this for a series of queries is a bit tricky.

#[no_mangle]
pub unsafe extern "cdecl" fn system_new(app: *mut App, request: SystemData) {
    let data = std::slice::from_raw_parts(request.queries, request.count)
        .iter()
        .map(|&ptr| *ptr)
        .collect::<Vec<_>>();

    let system = SystemBuilder::<()>::new((*app).world_mut())
        .builder::<Query<FilteredEntityMut>>(|query| {
            for component in &data {
                let id = ComponentId::new(component.id);
                let kind = &component.kind;

                match kind {
                    ComponentKind::Reference => {
                        query.ref_id(id);
                    }
                    ComponentKind::Mutable => {
                        query.mut_id(id);
                    }
                    ComponentKind::With => {
                        query.with_id(id);
                    }
                    ComponentKind::Without => {
                        query.without_id(id);
                    }
                }
            }
        })
        .build(move |query: Query<FilteredEntityMut>| {
            let mut count = 0usize;

            let components: Vec<*mut u8> = query
                .iter()
                .flat_map(|entity| {
                    count += 1;

                    data.iter()
                        .filter_map(|component| match component.kind {
                            ComponentKind::Reference | ComponentKind::Mutable => Some(
                                entity
                                    .get_by_id(ComponentId::new(component.id))
                                    .unwrap()
                                    .as_ptr(),
                            ),
                            _ => None,
                        })
                        .collect::<Vec<_>>()
                })
                .collect();

            let components_ptr = components.as_ptr();
            std::mem::forget(components);
            (request.callback)(QueryResult {
                components: components_ptr,
                count,
            });
        });

    (*app).add_systems(Update, system);
}

@chescock
Copy link
Contributor Author

Would also be nice if this supported Commands

Both the existing API and the one in this PR handle arbitrary SystemParams. You can get a Commands today using .param::<Commands>(), and with this PR using ParamBuilder. Is there something more you need there?


there is currently no way to query for Res/ResMut even though they can be inserted on the world by component id.

Maybe you want a buildable SystemParam that lets you access a filtered set of resources from the world? It should be possible to make something where you provide ComponentIds of resources to read and write in the builder, and then have methods on the param that check the access then get the resource from the UnsafeWorldCell. It would be a bit like like a FilteredEntityMut for resources.

I don't think this PR would make that easier or harder to implement.


currently this will only support one Query per system because doing this for a series of queries is a bit tricky.

This PR implements SystemParam for Vec, which might help if you want a Vec<Query>.

}

#[test]
fn param_set_vec_builder() {
Copy link
Member

Choose a reason for hiding this comment

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

Question: what's the point of being able to extract a vec of queries? This seems...generally unhelpful.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I plan to use it for scripting. The idea is to load a script at runtime that does any number of queries, and then build a script runner system with a Vec<Query<FilteredEntityMut>> based on those queries (or ParamSet<Vec<Query<FilteredEntityMut>>> if they might conflict). The script runner system would bind the queries in the script by indexing the Vec.

(Buildable Vec and ParamSet<Vec> are the features I actually wanted here, and it would have been straightforward to implement BuildableSystemParam for them. It just felt wrong to implement buildable ParamSet<Vec> without buildable ParmSet<(tuple,)>, and I didn't see a clean way to build tuples with a builder type.)

I'm happy to split it out to a follow-up PR if that makes this easier to review!

Copy link
Member

Choose a reason for hiding this comment

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

Yes please, I'd much prefer reviewing it as a separate PR :)

@alice-i-cecile alice-i-cecile added D-Complex Quite challenging from either a design or technical perspective. Ask for help! X-Contentious There are nontrivial implications that should be thought through D-Unsafe Touches with unsafe code in some way labels Jul 14, 2024
@@ -1427,6 +1378,136 @@ unsafe impl SystemParam for SystemChangeTick {
}
}

// SAFETY: `init_state` does no access. The interesting safety checks are on the `SystemParamBuilder`.
Copy link
Member

Choose a reason for hiding this comment

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

This safety comment isn't clear enough IMO. Make sure to tie back to the safety requirements of SystemParam explicitly.

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.

Neutral on the API changes. As is, mildly useful, as it allows for the construction of more complex system param like ParamSet. Generally well-made: good docs and tests.

I genuinely don't understand the value of the Vec SystemParam, and would like to cut it unless there's strong motivation. If it's borderline, please split it out into its own PR.

I quite like all three follow-ups, but I'd like them to each be their own PRs.

P.S. SystemState::build_system is particularly interesting 🤔 Definitely feels like the sort of tool that will get used for shenanigans eventually.

@alice-i-cecile alice-i-cecile added S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jul 14, 2024
@ScottKane
Copy link

there is currently no way to query for Res/ResMut even though they can be inserted on the world by component id.

Maybe you want a buildable SystemParam that lets you access a filtered set of resources from the world? It should be possible to make something where you provide ComponentIds of resources to read and write in the builder, and then have methods on the param that check the access then get the resource from the UnsafeWorldCell. It would be a bit like like a FilteredEntityMut for resources.

I don't think this PR would make that easier or harder to implement.

Yeah I think it wouldn't make sense for this PR, I think it's something that would be needed. From the rust docs for 0.14 I was going off Query and Local both being the only things that implement BuildableSystemParam. I think it would be nice to have Res/ResMut also implement this trait.

@chescock chescock added S-Needs-Review Needs reviewer attention (from anyone!) to move forward and removed S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Jul 30, 2024
Copy link
Contributor

@james-j-obrien james-j-obrien left a comment

Choose a reason for hiding this comment

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

Implementation looks good, although I don't love how it looks aesthetically the utility for composition of paramsets is there.

I think using ParamBuilder in this way will be a little confusing for new users, there's nothing in this snippet that tells me what that identifier is there for so without knowing how the feature works it reads strangely:

let system = (
    ParamBuilder,
    LocalBuilder(10),
    QueryParamBuilder::new(|builder| { builder.with::<B>(); }),
)
    .build_state(&mut world)
    .build_system(system);

Maybe including "default" in there somewhere could help.

Although I see in follow up PRs you can also express it more explicitly like this:
ParamBuilder::local::<u64>()
So I suppose it's not a huge deal.

@chescock
Copy link
Contributor Author

chescock commented Aug 7, 2024

Maybe including "default" in there somewhere could help.

Yeah, I don't think I quite got the names right, but I couldn't think of better ones. I'm happy to rename things if you have a suggestion!

@alice-i-cecile alice-i-cecile added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Aug 7, 2024
@alice-i-cecile alice-i-cecile added this pull request to the merge queue Aug 12, 2024
Merged via the queue into bevyengine:main with commit d4ec80d Aug 12, 2024
27 checks passed
rparrett pushed a commit to rparrett/bevy that referenced this pull request Sep 2, 2024
…vyengine#14962)

# Objective

Make the documentation for `SystemParamBuilder` nicer by combining the
tuple implementations into a single line of documentation.

## Solution

Use `#[doc(fake_variadic)]` for `SystemParamBuilder` tuple impls.


![image](https://github.com/user-attachments/assets/b4665861-c405-467f-b30b-82b4b1d99bf7)

(This got missed originally because bevyengine#14050 and bevyengine#14703 were open at the
same time.)
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-Feature A new feature, making something new possible D-Complex Quite challenging from either a design or technical perspective. Ask for help! D-Unsafe Touches with unsafe code in some way M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Contentious There are nontrivial implications that should be thought through
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants