owned this note changed 21 days ago
Published Linked with GitHub

Hot-patching Bevy code

MVP: must have

  • can hot reload ordinary once-per-frame systems
  • excellent docs explaining setup, limitations and troubleshooting
  • examples for common patterns and tasks
  • enabled / disabled via a single off-by-default feature flag
  • clear advice on how to set up a dev-mode flag for your project that enables this
  • better reporting from subsecond when a change can't be hotpatched and requires a full rebuild

MVP: nice to have

  • update and re-run Startup schedule systems by tracking the entities spawned and despawning them and re-running the system
  • resource value hot-reloading
    • update resource default values via .init_resource
    • update resource values via .insert_resource
  • hot reload observers
  • resources-as-components for smoother integration
  • bevy_cli integration to setup the environment as needed
  • reuse existing ecosystem work done by Dioxus's subsecond
  • dev setup chapter in new Bevy book that covers this
  • works on all major platforms
  • macros / annotations not required
  • workspace support to handle updates in crates other than the bin

MVP: out of scope

  • struct/enum migration for new or altered fields
  • hotpatching hooks
  • hotpatching global statics
  • plugin / system unloading
  • hotpatching system ordering

Open questions

  • just how much UB are we risking during development?
  • how should we track entities / setup done in plugins or in startup systems?
    • can we use temporary lifecycle observers to tag things?

Implementation strategy

  1. Polish bevy_simple_subsecond_system
    • improve docs
    • note limitations
  2. Fix any required subsecond issues
  3. Upstream it into Bevy.
    • off-by-default
    • dedicated crate with minimal dependencies

Prior art

End User API

this portion was initially written by L, to provide a reference point for discussion

The API for hot reloading should be as close as possible to "vanilla" bevy - some changes will be needed, but we should aim for them to be minimal and for the abstractions to disappear in production code.

Scoping Reload

Rust - as a compiled, statically typed language - is not set up well for reloading abitrary code. In addition, we don't necessarily want to incur the cost of enabling reload in areas that will rarely change - such as in crate dependencies. This suggests we should have a granular approach of reloading only things that changed.

On the flip side, if we only reload the minimal changes we risk having elements that rely on multiple versions of a struct running side by side, for example. So it's important that we expand the scope of reload to include everything that we want to be capable of change.

The bevy plugin ends up being a good middle ground here - it covers less area than the entire app, preventing the need to reload everything, and more area than a single system.

ridiculous_bevy_hot_reloading & bevy_simple_subsecond_system rely on marking a function as #[hot] to get it to reload, while dexterous_developer relies on creating a reloadable_scope!(|app| { /** Setup code **/}) that gets added from within a plugin.

However, we can set things up more directly in the plugin scope using one of the following approaches:

#[hot]
struct MyPlugin;

impl ReloadablePlugin for MyPlugin {
    fn (app: &mut ReloadableApp) { // ReloadableApp would have a subset of the capabilities of &mut App, to enforce only elements that support reloading in the plugin
        // implement your plugin here
    }
}

// when setting up the plugin
app.add_plugins(MyPlugin)

or

#[hot]
struct MyPlugin;

impl Plugin for MyPlugin {
    fn (app: &mut App) { // Here we can't enforce limitations on what is loaded
        
    }
}

// when setting up the plugin
app.add_reloadable_plugins(MyPlugin)

For the MVP, either option could work (and in fact, so would just #[hot] on a sysstem), since it doesn't require handling changes to data structures. However, I believe setting up with the first option will make things easier for the future.

Minimal Hotpatched Systems

comment by Jan Hohenheim

Alternatively, the boilerplate that the #[hot] annotation generates can also just be moved to the add_systems or IntoSystem implementation behind a feature gate / config, similar to how hot reloading works today. This way, all systems in the app will automagically benefit from hotpatching when a user turns that feature on. No annotations necessary, no new API is introduced.

comment by L note that this approach won't support adding/removing systems on the go. for that we would need to set up either the whole app or the plug-ins as reloadable. the main concern around reloading the whole app is loading assets into memory and re-running setup code (which could end up corrupting the current state you are trying to hot patch)

Select a repo