-
-
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
Plugin dependency graph v2 #11228
Plugin dependency graph v2 #11228
Conversation
It looks like your PR is a breaking change, but you didn't provide a migration guide. Could you add some context on what users should update when this change get released in a new version of Bevy? |
Generic plugins were discussed on discord, with the solution being to depend on specific instances. The part about |
Great point, and interesting question! Generic PluginsI'm not familiar with rapier's plugin structure. In general, this will only let you depend on a single specific concrete plugin. However, if you really want to depend on any member of a group of related plugins it may be possible to do interesting things by abusing the loose-coupling provided by // This is a plugin we would like to specify dependencies upon, regardless of the specific `T`.
struct MyGenericPlugin<T> {
// Plugin fields using T ...
};
// This marker trait doesn't implement `Plugin`, but plugins can still specify it as a dependency.
struct MyPluginMarker;
impl<T> Plugin for GenericPlugin<T> {
// Normal plugin stuff that requires T ...
fn subs_for(&self) -> Vec<TypeId> {
vec![TypeId::of::<MyPluginMarer>()]
}
} I believe this should allow a dependency on any struct MyDependentPlugin;
impl Plugin for MyDependentPlugin {
// Normal plugin stuff that doesn't know about T ...
fn depends_on(&self) -> Vec<TypeId> {
vec![TypeId::of::<MyPluginMarker>()]
}
} This is a consequence of letting the user specify a Non-unique pluginsMy current plan for non-unique plugins is to pretend, for the purposes of ordering and dependency resolution, that they are a single plugin. If any instance of a non-unique plugin specifies a dependency, then all instances of that plugin will be treated as having that dependency. They occupy a single node in the dependency graph, using the union across all their dependency sets, and are all built at the same time. Otherwise their behavior shouldn't change. I hope that's a bit clearer. |
Generic PluginsThe marker is an interesting approach, I like the aliasing aspect of it. To clarify what I thought the proposed solution on discord was: struct PhysicsPlugin<T> {
// `T` is used for custom user-data, not necessary for most users
};
impl<T> Plugin for MovementPlugin<T> {
fn depends_on(&self) -> Vec<TypeId> {
// `T` propagated to the other plugin
vec![TypeId::of<PhysicsPlugin<T>>()]
}
} Non-unique pluginsMisunderstanding on my part, the unique plugin functionality is already implemented, and the plugin instances of the same types never interact with one another. |
That's definitely the intended approach if both plugins are generic. I will add some examples up above. |
crates/bevy_app/src/plugin/mod.rs
Outdated
fn depends_on(&self) -> Vec<TypeId> { | ||
Vec::new() | ||
} |
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's your reasoning behind using a Vec
for the return value?
I feel like most dependencies will be known at compile time, so this could be a &'static [TypeId]
instead.
If you wanted to be able to dynamically specify dependencies, you could take advantage of Rust 1.75 and use impl Iterator<Item = TypeId>
.
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.
Thanks for the feedback! I'm not sure dependencies can always be known at compile time. Library authors might want to add flags to their plugin struct to enable or disable different functionality, and this might require them to change their dependencies.
Worth noting that I am currently reimplementing this PR from scratch, with a cleaner commit history. The new API I am playing with is:
fn configure(&self, &mut PluginManifest) -> {
manifest.add_dependency::<Plugin>(true); // required dependency
manifest.add_substitution::<Plugin>();
}
PluginManifest
is just a thin wrapper over HashMap<TypeId, PluginRelation>
. This design new significantly improves error reporting and simplifies logic about non-unique plugins. I believe it may also ease migration when/if we decide to transition plugins into the ECS as entities w/ relations.
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.
Interesting! That approach definitely seems a lot easier to use and extend. (It also decreases heap allocations, which is good!) Is your rewrite on a public branch? I'm curious to see what else you changed. :)
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 appreciate your interest! I have some uncommitted tests right now, but I will publish the new approach tomorrow or monday.
I hope to get the whole thing working later this week. This older approach started from dependencies and worked backwards, but I ran into issues with non-top-level plugin builds. My new approach starts from a general PluginSet
type which takes over the App::add_plugins
api and serves as the canonical representation of the Plugins<M>
trait:
let mut first_set = PluginSet::new();
set_ab.add_plugins(PluginA);
let mut second_set = PluginSet::new();
second_set.add_plugins((PluginGroup, (PluginB, PluginC), set_ab));
This fixes a bunch of issues in this branch, mostly at the cost of changing the PluginGroup
interface. All I have left is to stitch the two together.
fa63d2b
to
167c631
Compare
I didn't took a look into the implementation itself (yet), but I like the idea of having a better plugin dependency. My Use CaseWhat made me pick interest in this topic is the because I'm having a difficult time by managing plugin dependency with current plugin system. In short I need to add a custom Plugin Dependency APIEven tho I understand the proposed API I wonder if it is possible to make it match the Systems Dependency API, so instead of having app.add_plugins((
CustomAssetPlugin.before(AssetPlugin),
FancyPlugin.after(LogPlugin),
ConsoleDebugPlugin.add_if(is_debug_build),
)); That way it would be more straightforward for new users to just learn one API and would be less verbose to write IMO. Plugin SubstitutionI don't think it is a good idea to allow plugins to substitute others, this could cause a conflict and be hard to debug, because you don't know when some plugin that you added will substitute another plugin that you have added. A solution for this would be plugins to allow some |
This is the goal of the
The idea is that substitution is a contract. You should only substitute for a plugin if they are truly interchangeable, and provide the exact same resources for dependencies (if perhaps different behavior). This enables a ton of interesting features for modularity, like the ability to say "I need a physics plugin but I don't care which one" or "this plugin wraps and modifies bevy_core_pipeline". I would be fine with splitting it out into a S-Controversial follow-up if others feel strongly about this. |
This seems like important functionality both for the internal team to be able to try out new features or their variants without a full fork, and for the ecosystem to be able to contribute with lower barriers and less work for reviewers. It also lays the foundation for defining the internal API of Bevy as a network of contracts, and for supporting dependency injection of internal functionality across crate and feature boundaries. Basically, the ECS seems like a big black box and there is a lot of just taking another plugin's word for it that the data that just disappeared into the aether will be handled properly. A thin layer of formality as suggested by this PR could alleviate this concern in the short term by adding a slightly more fine-grained system of expressing dependencies than features. After some iterating, it could be developed into something that resembles more of a platform with a reference implementation engine that is quite usable out of the box, versus just the engine part. I also suspect that this is a good candidate for gradual adoption. I'd suggest to identify where this is most useful (physics springs to mind) and prototype how this actually works with that concrete example. |
This PR has promise but it far too large and sweeping. I'm going to try to break the useful bits into a few smaller incremental changes. |
This PR builds heavily off the initial work done by #7839. Thanks @maniwani <3.
Objective
Solution
The TL;DR is:
TypeId
.App::run
, and are sorted according to the dependency graph.Plugin::build
.This PR contains a few features which are too closely related for me to split into seperate PRs. I recommend reviewers start with the changes in
app.rs
, then move on to readPluginSet::order
inplugin.rs
before taking a look at the rest ofplugin.rs
.Manifests
Plugins may now implement
Plugin::configure
to specify relationships with other plugins.PluginManifest::add_dependency
tells the app to build another plugin before this one. If the dependency is marked required, the app will panic if the target plugin is disabled or not present.PluginManifest::add_substitution
disables the target plugin, if it is present, and makes all of it's dependencies depend on this plugin instead. It can be used for debugging, for overriding default engine behavior, or for plugin aliasing. A plugin cannot be overridden by multiple other plugins at the same time.When dealing with manifests, we pretend like all instances of a non-unique plugin are the same. A dependency on a non-unique plugin means all instances of that plugin will be built first. All instances of a plugin are passed a reference to the same
PluginManfiest
inPlugin::configure
. The same dependency can be specified multiple times, but the app will panic if, for example, one instance depends on a given plugin and another instance substitutes for it.PluginManifest
is basically just aHashMap<TypeId, PluginRelation>
, but it also stores the type names of the targets to provide friendly errors. The goal is to allow easy extensibility, and to pave the way forPlugin
s to move into the ECS once relations are in.Plugin Builds
The app now defers building plugins until
App::build
is called inApp::run
. Places whereApp:update
is called directly must now callApp::build
first. The plugin life-cycle functionsbuild
,ready
,finish
andclean
are now called in an order determined byPluginSet::order
which respects plugin dependencies.I have avoided changing any other aspects of the plugin life-cycle as much as possible. I think it would be worth re-evaluating when/if this is merged.
Plugin Sets
The elephant in the room is that
PluginGroup
is built to accommodate implicit plugin ordering, and makes no sense when plugins can be added in any order. This issue is confounded by theApp
now needing all plugins to be registered beforeApp::build
is called; Plugins likeTransformPlugin
which used to addValidParentCheckPlugin
inPlugin::build
now need to be added together. The simplest solution would be to add them to a newTrandomPluginGroup
, but plugin groups cant be added to plugin groups.To solve these problems I introduced
PluginSet
, a correct-by-default growable collection of plugins.PluginSet
takes over the existing functionality fromApp::add_plugins
and implementsPlugins
, allowing sets to be added to other sets.App
has been changed to hold aPluginSet
internally, andPluginGroup
now adds plugins directly to aPluginSet
rather than aPluginGroupBuilder
. Anything that implementsPlugins
can be turned into aPluginSet
, so it is effectively the canonical representation of that trait.PluginSet
is also in-charge of maintaining manifest information, and as I mentioned earlier is what determines the ordering of plugins for theApp
(throughPluginSet::order
).The downside is that every
PluginGroup
now needs to be re-written to use the new API, but the upside is that the API is a little less verbose and far more flexible.Changelog
TypeId
.App::run
, and are sorted according to the dependency graph.Plugin::build
.PluginSet
introduced, and used as the basis for a reworked plugin infrastructure.Migration Guide
App::update
must now callApp:build
first.Plugin::name
to control how they were identified will need to usesubs_for
instead.Plugin::name
has been removed.depends_on
.PluginGroup
must be re-written to usePluginSet
s, and calls toPluginGroup::build
should be removed.