Skip to content

Hot patching systems with subsecond #19309

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

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,9 @@ libm = ["bevy_internal/libm"]
# Enables use of browser APIs. Note this is currently only applicable on `wasm32` architectures.
web = ["bevy_internal/web"]

# Enable hotpatching of Bevy systems
hotpatching = ["bevy_internal/hotpatching"]

[dependencies]
bevy_internal = { path = "crates/bevy_internal", version = "0.16.0-dev", default-features = false }
tracing = { version = "0.1", default-features = false, optional = true }
Expand Down Expand Up @@ -4357,3 +4360,15 @@ name = "Extended Bindless Material"
description = "Demonstrates bindless `ExtendedMaterial`"
category = "Shaders"
wasm = false

[[example]]
name = "hotpatching_systems"
path = "examples/ecs/hotpatching_systems.rs"
doc-scrape-examples = true
required-features = ["hotpatching"]

[package.metadata.example.hotpatching_systems]
name = "Hotpatching Systems"
description = "Demonstrates how to hotpatch systems"
category = "ECS (Entity Component System)"
wasm = false
8 changes: 8 additions & 0 deletions crates/bevy_dev_tools/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ keywords = ["bevy"]
[features]
bevy_ci_testing = ["serde", "ron"]

hotpatching = [
"bevy_ecs/hotpatching",
"dep:dioxus-devtools",
"dep:crossbeam-channel",
]

[dependencies]
# bevy
bevy_app = { path = "../bevy_app", version = "0.16.0-dev" }
Expand All @@ -33,6 +39,8 @@ bevy_state = { path = "../bevy_state", version = "0.16.0-dev" }
serde = { version = "1.0", features = ["derive"], optional = true }
ron = { version = "0.8.0", optional = true }
tracing = { version = "0.1", default-features = false, features = ["std"] }
dioxus-devtools = { git = "https://github.com/DioxusLabs/dioxus", optional = true }
crossbeam-channel = { version = "0.5.0", optional = true }

[lints]
workspace = true
Expand Down
42 changes: 42 additions & 0 deletions crates/bevy_dev_tools/src/hotpatch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//! Utilities for hotpatching code.

use bevy_ecs::{event::EventWriter, HotPatched};
use dioxus_devtools::{connect, subsecond::apply_patch, DevserverMsg};

pub use dioxus_devtools::subsecond::{call, HotFunction};

use crate::{Last, Plugin};

/// Plugin connecting to Dioxus CLI to enable hot patching.
#[derive(Default)]
pub struct HotPatchPlugin;

impl Plugin for HotPatchPlugin {
fn build(&self, app: &mut crate::App) {
let (sender, receiver) = crossbeam_channel::bounded::<HotPatched>(1);

// Connects to the dioxus CLI that will handle rebuilds
// On a successful rebuild the CLI sends a `HotReload` message with the new jump table
// When receiving that message, update the table and send a `HotPatched` message through the channel
connect(move |msg| {
Copy link
Contributor

Choose a reason for hiding this comment

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

subsecond seems to provide hot-reload handler, could it be used here?

use dioxus_devtools::{connect_subsecond, subsecond::register_handler};

...
        connect_subsecond();
        register_handler(Arc::new(move || {
            sender.send(HotPatched).unwrap();
        }));

if let DevserverMsg::HotReload(hot_reload_msg) = msg {
if let Some(jumptable) = hot_reload_msg.jump_table {
// SAFETY: This is not unsafe, but anything using the updated jump table is.
// The table must be built carefully
unsafe { apply_patch(jumptable).unwrap() };
sender.send(HotPatched).unwrap();
}
}
});

// Adds a system that will read the channel for new `HotPatched`, and forward them as event to the ECS
app.add_event::<HotPatched>().add_systems(
Last,
move |mut events: EventWriter<HotPatched>| {
if receiver.try_recv().is_ok() {
events.write_default();
}
},
);
}
}
9 changes: 8 additions & 1 deletion crates/bevy_dev_tools/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![forbid(unsafe_code)]
#![cfg_attr(
feature = "hotpatching",
expect(unsafe_code, reason = "Unsafe code for system hotpatching.")
)]
#![cfg_attr(not(feature = "hotpatching"), forbid(unsafe_code))]
#![doc(
html_logo_url = "https://bevyengine.org/assets/icon.png",
html_favicon_url = "https://bevyengine.org/assets/icon.png"
Expand All @@ -13,6 +17,9 @@ use bevy_app::prelude::*;
#[cfg(feature = "bevy_ci_testing")]
pub mod ci_testing;

#[cfg(feature = "hotpatching")]
pub mod hotpatch;

pub mod fps_overlay;

pub mod picking_debug;
Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_ecs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ critical-section = [
"bevy_reflect?/critical-section",
]

hotpatching = ["dep:subsecond"]

[dependencies]
bevy_ptr = { path = "../bevy_ptr", version = "0.16.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [
Expand Down Expand Up @@ -124,6 +126,7 @@ variadics_please = { version = "1.1", default-features = false }
tracing = { version = "0.1", default-features = false, optional = true }
log = { version = "0.4", default-features = false }
bumpalo = "3"
subsecond = { git = "https://github.com/DioxusLabs/dioxus", optional = true }
Copy link
Member

Choose a reason for hiding this comment

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

Pretty sure git dependencies will make this unpublishable on crates.io


concurrent-queue = { version = "2.5.0", default-features = false }
[target.'cfg(not(all(target_has_atomic = "8", target_has_atomic = "16", target_has_atomic = "32", target_has_atomic = "64", target_has_atomic = "ptr")))'.dependencies]
Expand Down
10 changes: 10 additions & 0 deletions crates/bevy_ecs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ pub mod world;

pub use bevy_ptr as ptr;

#[cfg(feature = "hotpatching")]
use event::Event;

/// The ECS prelude.
///
/// This includes the most common types in this crate, re-exported for your convenience.
Expand Down Expand Up @@ -2768,3 +2771,10 @@ mod tests {
fn custom_clone(_source: &SourceComponent, _ctx: &mut ComponentCloneCtx) {}
}
}

/// Event sent when a hotpatch happens.
///
/// Systems should refresh their inner pointers.
#[cfg(feature = "hotpatching")]
#[derive(Event, Default)]
pub struct HotPatched;
7 changes: 6 additions & 1 deletion crates/bevy_ecs/src/observer/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -366,12 +366,17 @@ fn observer_system_runner<E: Event, B: Bundle, S: ObserverSystem<E, B>>(
};

// SAFETY:
// - `update_archetype_component_access` is called first
// - `update_archetype_component_access` is called before any world access
// - there are no outstanding references to world except a private component
// - system is an `ObserverSystem` so won't mutate world beyond the access of a `DeferredWorld`
// and is never exclusive
// - system is the same type erased system from above
unsafe {
// Always refresh hotpatch pointers
// There's no guarantee that the `HotPatched` event would still be there once the observer is triggered.
#[cfg(feature = "hotpatching")]
(*system).refresh_hotpatch();

(*system).update_archetype_component_access(world);
match (*system).validate_param_unsafe(world) {
Ok(()) => {
Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_ecs/src/schedule/executor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ impl System for ApplyDeferred {
Ok(())
}

#[inline]
fn refresh_hotpatch(&mut self) {}

fn run(&mut self, _input: SystemIn<'_, Self>, _world: &mut World) -> Self::Out {
// This system does nothing on its own. The executor will apply deferred
// commands from other systems instead of running this system.
Expand Down
15 changes: 15 additions & 0 deletions crates/bevy_ecs/src/schedule/executor/multi_threaded.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ use crate::{
system::ScheduleSystem,
world::{unsafe_world_cell::UnsafeWorldCell, World},
};
#[cfg(feature = "hotpatching")]
use crate::{event::Events, HotPatched};

use super::__rust_begin_short_backtrace;

Expand Down Expand Up @@ -443,6 +445,14 @@ impl ExecutorState {
return;
}

#[cfg(feature = "hotpatching")]
let should_update_hotpatch = !context
.environment
.world_cell
.get_resource::<Events<HotPatched>>()
.map(Events::is_empty)
.unwrap_or(true);

// can't borrow since loop mutably borrows `self`
let mut ready_systems = core::mem::take(&mut self.ready_systems_copy);

Expand All @@ -460,6 +470,11 @@ impl ExecutorState {
// Therefore, no other reference to this system exists and there is no aliasing.
let system = unsafe { &mut *context.environment.systems[system_index].get() };

#[cfg(feature = "hotpatching")]
if should_update_hotpatch {
system.refresh_hotpatch();
}

if !self.can_run(
system_index,
system,
Expand Down
23 changes: 23 additions & 0 deletions crates/bevy_ecs/src/schedule/executor/simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ use crate::{
},
world::World,
};
#[cfg(feature = "hotpatching")]
use crate::{event::Events, HotPatched};

use super::__rust_begin_short_backtrace;

Expand Down Expand Up @@ -60,6 +62,12 @@ impl SystemExecutor for SimpleExecutor {
self.completed_systems |= skipped_systems;
}

#[cfg(feature = "hotpatching")]
let should_update_hotpatch = !world
.get_resource::<Events<HotPatched>>()
.map(Events::is_empty)
.unwrap_or(true);

for system_index in 0..schedule.systems.len() {
#[cfg(feature = "trace")]
let name = schedule.systems[system_index].name();
Expand Down Expand Up @@ -120,6 +128,11 @@ impl SystemExecutor for SimpleExecutor {
#[cfg(feature = "trace")]
should_run_span.exit();

#[cfg(feature = "hotpatching")]
if should_update_hotpatch {
system.refresh_hotpatch();
}

// system has either been skipped or will run
self.completed_systems.insert(system_index);

Expand Down Expand Up @@ -186,6 +199,12 @@ fn evaluate_and_fold_conditions(
world: &mut World,
error_handler: ErrorHandler,
) -> bool {
#[cfg(feature = "hotpatching")]
let should_update_hotpatch = !world
.get_resource::<Events<HotPatched>>()
.map(Events::is_empty)
.unwrap_or(true);

#[expect(
clippy::unnecessary_fold,
reason = "Short-circuiting here would prevent conditions from mutating their own state as needed."
Expand All @@ -208,6 +227,10 @@ fn evaluate_and_fold_conditions(
return false;
}
}
#[cfg(feature = "hotpatching")]
if should_update_hotpatch {
condition.refresh_hotpatch();
}
__rust_begin_short_backtrace::readonly_run(&mut **condition, world)
})
.fold(true, |acc, res| acc && res)
Expand Down
23 changes: 23 additions & 0 deletions crates/bevy_ecs/src/schedule/executor/single_threaded.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ use crate::{
schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule},
world::World,
};
#[cfg(feature = "hotpatching")]
use crate::{event::Events, HotPatched};

use super::__rust_begin_short_backtrace;

Expand Down Expand Up @@ -60,6 +62,12 @@ impl SystemExecutor for SingleThreadedExecutor {
self.completed_systems |= skipped_systems;
}

#[cfg(feature = "hotpatching")]
let should_update_hotpatch = !world
.get_resource::<Events<HotPatched>>()
.map(Events::is_empty)
.unwrap_or(true);

for system_index in 0..schedule.systems.len() {
#[cfg(feature = "trace")]
let name = schedule.systems[system_index].name();
Expand Down Expand Up @@ -121,6 +129,11 @@ impl SystemExecutor for SingleThreadedExecutor {
#[cfg(feature = "trace")]
should_run_span.exit();

#[cfg(feature = "hotpatching")]
if should_update_hotpatch {
system.refresh_hotpatch();
}

// system has either been skipped or will run
self.completed_systems.insert(system_index);

Expand Down Expand Up @@ -204,6 +217,12 @@ fn evaluate_and_fold_conditions(
world: &mut World,
error_handler: ErrorHandler,
) -> bool {
#[cfg(feature = "hotpatching")]
let should_update_hotpatch = !world
.get_resource::<Events<HotPatched>>()
.map(Events::is_empty)
.unwrap_or(true);

#[expect(
clippy::unnecessary_fold,
reason = "Short-circuiting here would prevent conditions from mutating their own state as needed."
Expand All @@ -226,6 +245,10 @@ fn evaluate_and_fold_conditions(
return false;
}
}
#[cfg(feature = "hotpatching")]
if should_update_hotpatch {
condition.refresh_hotpatch();
}
__rust_begin_short_backtrace::readonly_run(&mut **condition, world)
})
.fold(true, |acc, res| acc && res)
Expand Down
5 changes: 5 additions & 0 deletions crates/bevy_ecs/src/system/adapter_system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ where
})
}

#[inline]
fn refresh_hotpatch(&mut self) {
self.system.refresh_hotpatch();
}

#[inline]
fn apply_deferred(&mut self, world: &mut crate::prelude::World) {
self.system.apply_deferred(world);
Expand Down
12 changes: 12 additions & 0 deletions crates/bevy_ecs/src/system/combinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,12 @@ where
)
}

#[inline]
fn refresh_hotpatch(&mut self) {
self.a.refresh_hotpatch();
self.b.refresh_hotpatch();
}

#[inline]
fn apply_deferred(&mut self, world: &mut World) {
self.a.apply_deferred(world);
Expand Down Expand Up @@ -417,6 +423,12 @@ where
self.b.run_unsafe(value, world)
}

#[inline]
fn refresh_hotpatch(&mut self) {
self.a.refresh_hotpatch();
self.b.refresh_hotpatch();
}

fn apply_deferred(&mut self, world: &mut World) {
self.a.apply_deferred(world);
self.b.apply_deferred(world);
Expand Down
5 changes: 5 additions & 0 deletions crates/bevy_ecs/src/system/exclusive_function_system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ where
})
}

#[inline]
fn refresh_hotpatch(&mut self) {
// TODO: support exclusive systems
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a particular blocker that made exclusive systems harder to implement?

Copy link
Member Author

Choose a reason for hiding this comment

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

no, mostly I wanted to make sure we wanted to go in this direction before doing more work.

it should also be a todo that can be picked up by anyone among the many other things that can be continued if we get this first step in

}

#[inline]
fn apply_deferred(&mut self, _world: &mut World) {
// "pure" exclusive systems do not have any buffers to apply.
Expand Down
Loading
Loading