Skip to content

Declarative plugins / Graph-based plugins MVP#24920

Draft
fallible-algebra wants to merge 7 commits into
bevyengine:mainfrom
fallible-algebra:declarative-plugins-mvp
Draft

Declarative plugins / Graph-based plugins MVP#24920
fallible-algebra wants to merge 7 commits into
bevyengine:mainfrom
fallible-algebra:declarative-plugins-mvp

Conversation

@fallible-algebra

@fallible-algebra fallible-algebra commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Objective

Get an MVP in of the declarative plugins implementation laid out in https://hackmd.io/@fallible/BJ_oByDcbl. Opening this as a draft for my own sake + a bit more visibility, but it's not in a seriously reviewable state just yet.

Principle

Plugin registration adds stuff directly to an App, which hands stuff directly off to World. This is a procedural solution to what is mostly a "build system" problem. Build systems use graphs and solvers to juggle the needs of a program and manage complex constraints.

We can do something similar here by moving the API surface of plugin registration away from App and towards a "Manifest"-style data structure of preferences and approval/veto functions for configurations of plugins and resources, then find a solution for a whole dependency tree of plugins that everyone in the supply chain agrees on / doesn't veto.

Short-term Motivations:

  1. Straightforward Plugin dependencies implementation1. (Plugin Dependencies #69)
  2. Make plugin/resource conflicts user/library maintainer solvable.
  3. Simplify the plugin API surface for users.

Long-term Motivations:

  1. Entry point for collecting editor/analysis-relevant bevy app information without running the program itself.
  2. Add/remove systems supported in hot-reloading contexts.
  3. Allow data-driven overrides of plugin configuration i.e. config file says system with the name my_cool_system runs in Last not Update, and this gets applied before World is constructed and the App is run.
  4. "editor tool" registration methods alongside "running app" registration methods.
  5. Generally support "here's the graph of what the app will probably look like, do something with that" tooling.
    1. Order independence of App construction #1255 mentions this as desirable.
  6. Enables for "BSN should know about this, but don't put it into the ECS" style registration.
  7. solve world hunger, put £100,000,000 in everyone's bank account without causing inflation, reverse hair greying.

Solution

  1. Introduce a DeclarativePlugin trait that is a drop-in replacement for most users needs.
  2. Each plugin creates a collection of items to add to an app.
  3. Cycles-allowed Digraph is built
  4. Dependencies between plugins are evaluated and an appropriate candidate plugin config is chosen.
  5. Resource preferences are evaluated and appropriate values are chosen.
  6. Acyclic Digraph is built now the cycles have been severed.
    1. In the event of no solution, error for the user and close the program.
  7. Topsort gives an order to register items with the underlying App

Before leaving draft (TODO: Remove)

  • Recreate the majority of the current Plugin/registration-facing App API, but over a PluginOutput type that stores the systems, observers, resources, messages, and schedule labels instead of directly adding them to an app.
  • Write the graph solving & "apply to App" logic
  • move the unsafe stuff out of bevy_app
  • Test suites.
  • More + shorter explanations in this PR body.
  • Dependencies on non-declarative plugins.
  • Precedence for "source" of the plugin or resource (Default < Library < App, prefer higher) so that a default value isn't chosen over a value given by someone building a game.
    • "Distance from being manually added" as part of this.
  • Find all the issues relevant to this PR
  • Make Hashable a supertrait of DeclarativePlugin to prevent infinite dependency expansion (Plugin A --> Plugin B --> Plugin C --> Plugin A)?

Not for this MVP

  • See if rendering dev needs can be met with this design
    • Rendering-focused plugins tend to go directly into the RenderApp's guts to set up rendering systems.
    • Maybe it's good enough? Maybe it falls short.
  • Perfect naming of concepts
    • DeclarativePlugin is clunky, but it should become the main Plugin trait and that plugins that really need access to an underlying App before it's run as a program will use an legacy, renamed Plugin trait or lobby for their needs being met by the declarative API.

FAQ

Please raise any concerns / suggestions, this is a draft.

  • "I need to do something weird to World in plugin registration, what do I do?"
    • See if it can be moved to a Startup system, or stick with whatever the current Plugin will be. Alternatively, please state the use case here it can be accounted for in design.

Worries / Scope

This PR might be trying to do too many things. An actual MVP might just create DeclarativePlugin with a PluginOutput that wraps an App without giving an escape hatch for direct access and mark Plugin as deprecated while opening a FoundationalPlugin to maintain the behaviour of Plugin with App access.

Testing

TODO


Showcase

TODO: example of dependencies between plugins

Footnotes

  1. The bevy ecosystem cannot at this time develop crates that have complex plugin dependency needs without asking the user to do some configuration themselves. Plugins that eagerly add 3rd party plugins to a bevy app panic when a plugin is added more than once.

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

The generated examples/README.md is out of sync with the example metadata in Cargo.toml or the example readme template. Please run cargo run -p build-templated-pages -- update examples to update it, and commit the file change.

@alice-i-cecile alice-i-cecile added A-App Bevy apps and plugins X-Needs-SME This type of work requires an SME to approve it. M-Release-Note Work that should be called out in the blog due to impact labels Jul 9, 2026
@alice-i-cecile alice-i-cecile added the C-Feature A new feature, making something new possible label Jul 9, 2026
@alice-i-cecile

alice-i-cecile commented Jul 9, 2026

Copy link
Copy Markdown
Member

#69 and #1255 are also relevant and should be linked in the description :)

@alice-i-cecile

Copy link
Copy Markdown
Member

"I need to do something weird to World in plugin registration, what do I do?"

My take here is mostly "don't do that". I'm very reluctant to support this sort of pattern in general: states and startup systems are better, and there's nothing stopping people from just manually writing code that modifies App before run. There's nothing stopping users from writing their own ad hoc Plugin trait externally that perfectly mirrors the previous imperative/opaque behavior.

My initial impressions are that you're being too careful to avoid breakage for weird users here, when 90% of the cases should be handled just fine by declarative patterns, and 90% of the remainder are patterns that should be rethought completely :)

WRT rendering it feels like a trait RenderPlugin: Plugin with some specialized methods would go a long way to reducing boilerplate and standardizing patterns, and allow us to refactor much more easily in the future if we want to e.g. move away from SubApps there.

@alice-i-cecile alice-i-cecile added M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged D-Complex Quite challenging from either a design or technical perspective. Ask for help! labels Jul 9, 2026
@fallible-algebra

Copy link
Copy Markdown
Contributor Author

My initial impressions are that you're being too careful to avoid breakage for weird users here

The plan is definitely this way of doing plugins becoming the only way of doing plugins with no escape hatches. What I want to do here is keep scope small, then expand it in follow-up PRs, mark the old API as deprecated, and then replace Plugin entirely. Mainly want to avoid stepping on people's toes with a sudden full migration to something that would require more than a moment's thought, as this is a point in the API that everyone's projects touch and could cause frustration / userbase churn.

trait RenderPlugin: Plugin with some specialized methods would go a long way to reducing boilerplate and standardizing patterns

Yeah I'm still cooking in my head about how to manage different plugin "contexts" like render plugin / editor plugin. This is a fair shout.

@NthTensor

Copy link
Copy Markdown
Contributor

oh neat!

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

The generated examples/README.md is out of sync with the example metadata in Cargo.toml or the example readme template. Please run cargo run -p build-templated-pages -- update examples to update it, and commit the file change.

@alice-i-cecile

Copy link
Copy Markdown
Member

I can live with a gradual approach :)

Proposed migration plan:

  • Version X: Plugin deprecated, ImperativePlugin and DeclarativePlugin created. All or almost all internal plugins migrated.
  • Version X + 1: DeclarativePlugin deprecated, Plugin reintroduced as the new name for DeclarativePlugin, ImperativePlugin deprecated.
  • Version X + 2: DeclarativePlugin and ImperativePlugin removed.

My kingdom for trait aliases, but maybe you can make it easier with blanket impls.

@cart

cart commented Jul 10, 2026

Copy link
Copy Markdown
Member

I can live with a gradual approach :)

I personally think plugins are too central (and future breaking changes to them too far reaching) to just wing it. Imo this deserves a working group with lots of adversarial design. We should have buy in from as many Bevy devs as possible and high confidence in our path across many scenarios. I do think this PR is a good conversation starter / a solid working group kickoff item, but I don't think the need is so pressing that we need to move quickly rather than surely here.

Lots of open questions:

  • Declarative data model (what is the final "picture" we're constructing and how do we construct it)
  • Plugins as entities?
  • How will this interact with "dynamic plugins" / can it be the foundation for them?
  • Surface level UX (optional trait methods vs builder pattern, short and slim (self.resource::<A>(), self.systems()) vs explicit (self.require_resource, self.add_systems). This is our next "big" opportunity to improve the Bevy API UX and we should take it across as many dimensions as possible.

@cart

cart commented Jul 10, 2026

Copy link
Copy Markdown
Member

I can live with a gradual approach :)

I failed to read this in the appropriate context. I agree with your assessment of how the trait rollout should happen.

(I do still think this is working group material / high-scrutiny work)

@fallible-algebra

fallible-algebra commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

Heya, thanks for taking a look! I am writing these answers down mostly for my sake, but I hope they're helpful 😅

I personally think plugins are too central (and future breaking changes to them too far reaching) to just wing it [...] I don't think the need is so pressing that we need to move quickly rather than surely here.

I agree, I've also been stewing on this for 4+ months including a heavily-edited and now mangled design doc (I got frustrated partway through an additional round of editing it and decided to just take a stab at impl).

I'm fine with work in this PR being too dubious to merge and in need of more discussion, but I really want to work out the implementation details because it's been weighing on me. I've struggled to convince people outside of alice and a few others.

It's been suggested by @MalekiRe this could be a 3rd party crate for now given it doesn't actually cause any bevy-internal changes, and I might move it to there if it makes more sense. But yeah I do want to get the conversation actually going :)

[1] Declarative data model

We're building a graph of "plugin output" nodes and dependency edges. We then expand that into a graph of "items that can be added to world" through a solving step which "asks" each plugin for approval wrt resources and plugin configs.

What the end user1 sees is methods on a type that is otherwise opaque to them. They have a limited set of operations they can perform, we have an underlying type we're free to change without end user scrutiny.

The data model should not be plugin-author-accessible outside of the additive methods on the opaque type, imo. I'm of the opinion that users and plugin authors that care about internals should be registering startup systems, not doing startup logic in plugin registration.

I can see points of contention there, but I think there are real benefits to keeping plugin registration about exposing information (including where the initialisation code is) rather than directly running initialisation code. As alice said above, the places where people are currently doing direct world/subapp access could have needs met by a declarative API. It'd just take some more awareness of what the needs are for that use.

[2] Plugins as entities?

I chatted a bit with @ItsDoot today on the discord and I'm on the fence about there being a contradiction between the two efforts.

On one hand, being able to use patches on resources (and plugins) is very cool, and using a World to represent the relationships between plugins is also really cool.

On the other hand, this might be a data representation question rather than an API design question.

At this point in time I think both can live together. I also think that exposing plugins as entities as the entry point for plugin registration might prevent us from being able to make confident assertions about deep, interconnected plugin dependency graphs "just working."

Being able to hide World from plugin registration and have a intermediate step where we make decisions about what to do with the program before running it is desirable, and my take on this is that this requires plugin registration to genuinely not be able to touch World at all, and plugins-as-entities as the API surface would break that by giving varying levels of World access in hooks, observers, and bsn template calls.

Even if PluginWorld is different to the World we actually run our program from, it still gives spots where a plugin author can decide they're going to do things the end user may not expect (like erase systems or resources)2. But I'm curious about what @ItsDoot's thoughts are here.

[3] How will this interact with "dynamic plugins" / can it be the foundation for them?

I would hope it's the start of something enabling, as it's a move from plugins being about adding data directly to world towards building a metadata-rich graph to reason about.

Hypothetically it's a spot where an API surface can be built, and a space where we can build an bevy-internal model for what to do with dynamic plugins.

I do not have the answers for how implementation should look. It was on my mind, but not enough that I have an answer. I'm guessing the answer depends on if we can expose something consistent enough for dynamic plugins to be able to expose the data (function pointers) and metadata (type information etc.) needed to register what they need to. I think an opaque-to-the-end-user data type with a well-defined registration API is still the key here. I could be way off base here (and probably am).

[4] Surface level UX [...]

First priority for what I'm doing here is making it smooth for developers to move from Plugin to DeclarativePlugin and for behaviour to be (mostly) the same (but be able to take advantage of the config approval tools).

Later I want to see the plugin API change after that (because it really needs it) but as I said above I don't want to cause bevy to lose users because there was a sudden massive change to the API everyone's stuff touches that people need to think about for more than a second :)

Footnotes

  1. "End user" here being "person developing a bevy app that runs" rather than "engine dev" "user of app/game" or "plugin author"

  2. it could be that erasing systems and resources from a graph is desirable. This should happen in a dedicated "analysis step" with its own entry points for plugins and end users imo.

@cart

cart commented Jul 11, 2026

Copy link
Copy Markdown
Member

Copy/pasting this from Discord:

I do think we need more explicit stated motivation before moving forward with any plugin changes. Iirc our motivations were:

  1. Allow plugins to depend on other plugins, rather than dependencies being "implied". Deferred init is one way to do this, but as Sander stated, this isn't necessary / it could be done with an app.init_plugin(X) API. We already have this in the form of is_unique checks in add_plugins, and we already take advantage of this. This does get more complicated if we want plugins to be able to configure other plugins, as configuration could be defined by a "future" plugin later. Thats where deferred starts to feel necessary. I'm also starting to lean in the direction of using Bevy Settings for plugin configuration, as this makes plugins (including important things like window defaults) easily configurable by the editor. That builds a clearer / standard path for a setting hot-reloading lifecycle.
  2. Solve order-dependence for things like configuration resources. register_asset_source() comes to mind (it must be configured before DefaultPlugins or the asset server won't have that asset source). I believe configuring the wgpu device init could also fall into this category (ex: letting plugins declare their gpu feature needs before the render device is initialized). What other cases are there? How much of a problem is this?
  3. Make it possible to unload / reload / hot-reload plugins. This requires building a mapping between plugins and the things they create. One strategy is a declarative / deferred API, but it could just as easily be accomplished by a wrapper that tracks changes as initialization occurs.
  4. Support the idea of a "plugin lifecycle" more effectively. Right now we have things like Plugin::finish() to run logic after all of the other plugins have run (and the renderer is "ready"). Is this enough? What does the ideal API look like?

I'm personally dubious of things like "plugins swapping out other plugin's systems". I've never really been a fan of the "reach in and do arbitrary things to the schedule" stuff. Systems aren't generally a public API surface. They (in combination) produce a set of behaviors. It should be up to the author of the systems to build "configuration levers" for the people that want them (ex: public system sets, settings to disable features in controlled ways, etc).

Comment thread examples/app/declarative_plugins.rs Outdated

impl DeclarativePlugin for PluginB {
fn build(&self, output: &mut bevy::app::PluginOutput) {
output.add_dependency_no_worries::<PluginA>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This example should show an actual dependency

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We're in drafts still, that's the plan later ^^

Update the example
Change up the Approval type a bunch
@fallible-algebra fallible-algebra force-pushed the declarative-plugins-mvp branch from 5d72e1f to d3e42f5 Compare July 11, 2026 20:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-App Bevy apps and plugins C-Feature A new feature, making something new possible D-Complex Quite challenging from either a design or technical perspective. Ask for help! M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide M-Release-Note Work that should be called out in the blog due to impact S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged X-Needs-SME This type of work requires an SME to approve it.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Plugin Dependencies

5 participants