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

Dynamic Systems and Components #623

Open
wants to merge 3 commits into
base: master
from

Conversation

@zicklag
Copy link
Contributor

@zicklag zicklag commented Oct 4, 2020

Related to: #32
Resolves: #142

Note: How this is related to #32 ( StableTypeId discussion ) this is related to #32 because it changes the TypeId struct used to identify components, but it does not directly address the problem of dynamic loading of Rust plugins. See #32 (comment).

This PR will attempt to establish the ability to create systems and components that have been determined at runtime instead of compile time. I'm going to try to implement this in one PR because I won't be sure about the sufficiency of the design until I actually get a working example of a dynamically registered system and component ( which I will include with this PR ).

Note: If we want to merge pieces of this PR one at a time, I am perfectly fine with that. I am making sure that each step is cleanly separated into it's own commit and can easily be ported to an individual PR.

I'm going to try to attack this problem one step at a time:

Steps

Non-Rust Component Ids

Status: Completed in commit: "Implement Custom Component Ids"

Currently bevy_hecs uses std::any::TypeId to uniquely identify the component IDs, but this does not allow us to define components that have a non-Rust origin. The first commit in the PR has migrated all of the internals of the bevy ECS to use a new ComponentId instead that is defined as:

/// Uniquely identifies a type of component. This is conceptually similar to
/// Rust's [`TypeId`], but allows for external type IDs to be defined.
#[derive(Eq, PartialEq, Hash, Debug, Clone, Copy)]
pub enum ComponentId {
    /// A Rust-native [`TypeId`]
    RustTypeId(TypeId),
    /// An arbitrary ID that allows you to identify types defined outside of
    /// this Rust compilation
    ExternalId(u64),
}

Establish Dynamic Queries

Status: Completed in commit: "Implement Dynamic Systems"

This adds a new state type parameter to the Fetch trait. This allows compile-time constructed queries like to be constructed with State = () and runtime constructed queries with the state parameter set to a DynamicComponentQuery.

Establish Dynamic Component Insertion

Status: Completed in commit: "Add Dynamic Component Support"

This adds a new implementation of the Bundle trait, RuntimeBundle which can be used to insert untyped components at runtime by providing a buffer of bytes and the required type information.

Create Examples

Status: Completed in respective commits

We also have new examples for dynamic_systems and dynamic_components.

Remaining Work

The biggest thing necessary at this point is a solid review. There's a lot of code changed, but thankfully not a ton of new logic. The bulk of the changes are repetitive changes required to add the necessary type parameters and such for the new Fetch `State type parameter.

Otherwise, there are a couple of todo!()'s in bevy_scene because I don't know enough about the Bevy scene architecture to integrate scenes with external components yet. I think that could reasonably be left to a separate PR, but I'm fine either way.

if let Some(component_registration) =
component_registry.get(&match type_info.id() {
bevy_ecs::ComponentId::RustTypeId(id) => id,
_ => todo!("Handle external ids in component registry"),

This comment has been minimized.

@zicklag

zicklag Oct 4, 2020
Author Contributor

This may need to be fixed before merging. We have to find out how we want to manage the scene registry when it comes to external types. I don't have a full understanding of how it works currently so I didn't implement anything yet

I think I'm going to prioritize figuring out the dynamic component and systems approach before I try to fix this.

@zicklag
Copy link
Contributor Author

@zicklag zicklag commented Oct 6, 2020

Just an FYI, @cart probably don't waste time reviewing this yet ( even if you get time ) as I found some stuff that might need to change a little as I keep going. I'll mark the PR as not a draft when it's ready for review.

@zicklag zicklag force-pushed the katharostech:feature/dynamic-systems-and-components branch 4 times, most recently from d6c3844 to 7e44b37 Oct 6, 2020
@zicklag zicklag force-pushed the katharostech:feature/dynamic-systems-and-components branch from f4ca66f to d9e07fb Oct 10, 2020
@zicklag zicklag marked this pull request as ready for review Oct 10, 2020
@zicklag
Copy link
Contributor Author

@zicklag zicklag commented Oct 10, 2020

OK, this is still probably a little rough around the edges, but I think the concept is laid out enough to get some review by anybody who is able to look at this. If any community members want to comment on this it'd be good to get as much feedback as possible before Carter has a chance to look at him to save him some time if there are glaring issues. 😄

@zicklag zicklag force-pushed the katharostech:feature/dynamic-systems-and-components branch 2 times, most recently from 1f901da to 3dccbc9 Oct 16, 2020
@zicklag
Copy link
Contributor Author

@zicklag zicklag commented Oct 16, 2020

Hey, CI is passing now. :)

I just ran my new benchmarks on this PR and the result is ( PR in blue, master in red ):

image

The frame time and the CPU cycles differences are within the noise noise range on my machine ( my laptop with a web browser running on it ), but the CPU instructions are always stable pretty much within 0.01% when there are no changes so this PR clearly represents an increase of at least 1.85% increase in CPU instructions for the benchmark games. I was hoping to avoid slowing down non-scripted games, such as the current benchmark games, at all, but it looks like the new design has a slight overhead. Not sure, how much that means though.

@zicklag zicklag force-pushed the katharostech:feature/dynamic-systems-and-components branch 5 times, most recently from a362d2e to c4b591e Oct 18, 2020
@zicklag
Copy link
Contributor Author

@zicklag zicklag commented Oct 19, 2020

I've now successfully demonstrated both dynamic systems and dynamic_components summing up, I think, all that is necessary to allow 3rd party implementations of scripting systems for Bevy! 🎉

The examples demonstrating both dynamic systems and dynamic components are very limited in scope and do not prove that there aren't bugs when doing queries over multiple archetypes or all kinds of other variables, but for the most part I've built on the existing Bevy logic wherever possible and there is as little new logic as possible, which should help for review and stability.

I'm going to start working on a language agnostic scripting system on top of these new features. This will stay separate from Bevy, but it will help stress test these new features once I get it going.

@cart
Copy link
Contributor

@cart cart commented Oct 20, 2020

Awesome work! I'm curious to hear what @Ralith would think about upstreaming these changes.

@Ralith
Copy link

@Ralith Ralith commented Oct 20, 2020

I'm open to the idea, though I'd need to do a proper review. The usecase is well-justified. A few initial thoughts:

RuntimeBundle

Could this be folded into EntityBuilder?

ComponentId

Maybe a custom Hash impl that discards the enum tag could help recover some of the perf here? I'm not all that concerned about an apparently purely-on-paper overhead, though.

@zicklag
Copy link
Contributor Author

@zicklag zicklag commented Oct 20, 2020

Could this be folded into EntityBuilder?

Oh, that's perfect. I had missed the EntityBuilder. I'll update that. I really hated the name RuntimeBundle anyway. 😛

Maybe a custom Hash impl that discards the enum tag could help recover some of the perf here?

That's a good idea, I'll try that out. I'll will add that I'm pretty sure that with these changes the ECS bench iteration speed is not decreased when using static systems.

As far as the iteration speed of dynamic systems ( or, probably more accurately, stateful queries ), the last I tried the ECS bench with a dynamic system the iteration speed when from ~1µs to ~100µ compared to the non-dynamic version. I'm not sure how accurate that benchmark was, though.

At this point I figured that we can be reasonably sure that we haven't lost a, so far, noticeable amount of performance for non-scripted systems. If we notice scripted systems, or stateful queries or something are slow, we can try to optimize it later after it is actually working, being that the non-dynamic features already there don't appear slower because of the new features.

@zicklag
Copy link
Contributor Author

@zicklag zicklag commented Oct 20, 2020

Using a custom hash implementation and benchmarking just the hash with criterion yields a small performance increase:

hash rust id            time:   [7.2598 ns 7.3019 ns 7.3501 ns]                          
                        change: [-7.3700% -6.4855% -5.5781%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 8 outliers among 100 measurements (8.00%)
  4 (4.00%) high mild
  4 (4.00%) high severe

hash external id        time:   [7.2684 ns 7.2878 ns 7.3092 ns]                              
                        change: [-9.0916% -8.1682% -7.3739%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 6 outliers among 100 measurements (6.00%)
  3 (3.00%) high mild
  3 (3.00%) high severe

Does this look right for a hash implementation? I'm not sure if there is a better way to do this that avoids the match to avoid the branch while still making the hash of RustTypeId unique compared to ExternalId. In this case I just write an extra byte as either a 1 or a 0 to distinguish them from each-other. Maybe that one byte is smaller than the discriminate used in the derived hash implementation and that's why it's faster:

#[derive(Debug, Clone)]
pub enum ComponentId {
    RustTypeId(std::any::TypeId),
    ExternalId(u64),
}

impl Hash for ComponentId {
    fn hash<H: Hasher>(&self, state: &mut H) {
        match self {
            ComponentId::RustTypeId(id) => {
                id.hash(state);
                state.write_u8(0);
            }
            ComponentId::ExternalId(id) => {
                state.write_u64(*id);
                state.write_u8(1);
            }
        }
    }
}

Edit: Opened a forum topic just to get more feedback.

Also it doesn't effect the CPU instructions measurement noticeably for the asteroids and breakout benchmarks.

@zicklag
Copy link
Contributor Author

@zicklag zicklag commented Oct 20, 2020

OK, I got help on the forum that said that the hashes are allowed to collide, so we can totally get rid of any discriminant between RustTypeId and ExternalId ( which is probably exactly what you were meaning @Ralith, I just didn't know that hashes were allowed to collide, so I didn't think of that ). With that optimization we get a decent speed up:

impl Hash for ComponentId {
    fn hash<H: Hasher>(&self, state: &mut H) {
        match self {
            ComponentId::RustTypeId(id) => {
                id.hash(state);
            }
            ComponentId::ExternalId(id) => {
                state.write_u64(*id);
            }
        }
    }
}
hash rust id            time:   [5.3033 ns 5.3093 ns 5.3154 ns]                          
                        change: [-33.204% -32.628% -31.900%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 12 outliers among 100 measurements (12.00%)
  1 (1.00%) low severe
  3 (3.00%) low mild
  3 (3.00%) high mild
  5 (5.00%) high severe

hash external id        time:   [4.5068 ns 4.5136 ns 4.5208 ns]                              
                        change: [-43.353% -42.849% -42.378%] (p = 0.00 < 0.05)
                        Performance has improved.
Found 10 outliers among 100 measurements (10.00%)
  4 (4.00%) high mild
  6 (6.00%) high severe

Hey that also brings down the CPU instructions a little bit in the game benchmarks. Still a little higher, but not as much as before.

image

@zicklag zicklag force-pushed the katharostech:feature/dynamic-systems-and-components branch 3 times, most recently from ecd08b7 to a383a18 Oct 20, 2020
@zicklag zicklag force-pushed the katharostech:feature/dynamic-systems-and-components branch 4 times, most recently from 4725ace to d8ea798 Nov 1, 2020
@zicklag
Copy link
Contributor Author

@zicklag zicklag commented Nov 2, 2020

OK, I've gotten this updated with the latest Bevy changes. ( I've still got cleanup to do ) I switched back to making Fetch stateful. With the simplification of the Query iterator that came with #741 the amount of complication that stateful Fetch added was much lower thankfully. With lockless queries being implemented I didn't want to have to maintain a separate query implementation for dynamic queries so I think that the little bit of extra generics that comes with having stateful queries is worth it. This may also help us with stateful queries for order independent change detection later, too.

@zicklag zicklag force-pushed the katharostech:feature/dynamic-systems-and-components branch from bb0ed18 to 09d2e0f Nov 3, 2020
This replaces the use of TypeId for identifying components and instead
uses ComponentId which is an enum that can represent either a Rust
TypeId or an external id represented by a u64.
@zicklag zicklag force-pushed the katharostech:feature/dynamic-systems-and-components branch 15 times, most recently from 311b639 to 07a44c0 Nov 3, 2020
@zicklag zicklag force-pushed the katharostech:feature/dynamic-systems-and-components branch from 07a44c0 to 9111e06 Nov 4, 2020
@zicklag
Copy link
Contributor Author

@zicklag zicklag commented Nov 4, 2020

OK, I did the cleanup I needed to do and went over the code one more time and I think it's ready for review, now.

@zicklag zicklag marked this pull request as ready for review Nov 4, 2020
Cargo.toml Outdated Show resolved Hide resolved
@zicklag zicklag force-pushed the katharostech:feature/dynamic-systems-and-components branch from 68e588f to 40a70b4 Nov 5, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

6 participants
You can’t perform that action at this time.