diff --git a/Cargo.toml b/Cargo.toml index 9602ef33fbb58..8a50b4b4784b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 } @@ -4401,3 +4404,15 @@ name = "Cooldown" description = "Example for cooldown on button clicks" category = "Usage" wasm = true + +[[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 diff --git a/crates/bevy_app/Cargo.toml b/crates/bevy_app/Cargo.toml index c892860dcec0b..5a999ea64b4de 100644 --- a/crates/bevy_app/Cargo.toml +++ b/crates/bevy_app/Cargo.toml @@ -71,6 +71,12 @@ web = [ "dep:console_error_panic_hook", ] +hotpatching = [ + "bevy_ecs/hotpatching", + "dep:dioxus-devtools", + "dep:crossbeam-channel", +] + [dependencies] # bevy bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } @@ -87,6 +93,8 @@ variadics_please = "1.1" tracing = { version = "0.1", default-features = false, optional = true } log = { version = "0.4", default-features = false } cfg-if = "1.0.0" +dioxus-devtools = { version = "0.7.0-alpha.1", optional = true } +crossbeam-channel = { version = "0.5.0", optional = true } [target.'cfg(any(unix, windows))'.dependencies] ctrlc = { version = "3.4.4", optional = true } diff --git a/crates/bevy_app/src/hotpatch.rs b/crates/bevy_app/src/hotpatch.rs new file mode 100644 index 0000000000000..1f9da40730e03 --- /dev/null +++ b/crates/bevy_app/src/hotpatch.rs @@ -0,0 +1,42 @@ +//! Utilities for hotpatching code. +extern crate alloc; + +use alloc::sync::Arc; + +use bevy_ecs::{event::EventWriter, HotPatched}; +#[cfg(not(target_family = "wasm"))] +use dioxus_devtools::connect_subsecond; +use dioxus_devtools::subsecond; + +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::(1); + + // Connects to the dioxus CLI that will handle rebuilds + // This will open a connection to the dioxus CLI to receive updated jump tables + // Sends a `HotPatched` message through the channel when the jump table is updated + #[cfg(not(target_family = "wasm"))] + connect_subsecond(); + subsecond::register_handler(Arc::new(move || { + 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::().add_systems( + Last, + move |mut events: EventWriter| { + if receiver.try_recv().is_ok() { + events.write_default(); + } + }, + ); + } +} diff --git a/crates/bevy_app/src/lib.rs b/crates/bevy_app/src/lib.rs index 743806df71b6f..9d36bfe67486b 100644 --- a/crates/bevy_app/src/lib.rs +++ b/crates/bevy_app/src/lib.rs @@ -34,6 +34,9 @@ mod task_pool_plugin; #[cfg(all(any(unix, windows), feature = "std"))] mod terminal_ctrl_c_handler; +#[cfg(feature = "hotpatching")] +pub mod hotpatch; + pub use app::*; pub use main_schedule::*; pub use panic_handler::*; diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index bf71d217d466f..960106666c55a 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -83,6 +83,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 = [ @@ -117,6 +119,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 = { version = "0.7.0-alpha.1", optional = true } 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] diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index d8846b29a1b83..7dfb60292cc15 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -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. @@ -123,6 +126,13 @@ pub mod __macro_exports { pub use alloc::vec::Vec; } +/// Event sent when a hotpatch happens. +/// +/// Systems should refresh their inner pointers. +#[cfg(feature = "hotpatching")] +#[derive(Event, Default)] +pub struct HotPatched; + #[cfg(test)] mod tests { use crate::{ diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index 520147d4385e9..43ece18ff5151 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -371,6 +371,11 @@ fn observer_system_runner>( // 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(); + match (*system).validate_param_unsafe(world) { Ok(()) => { if let Err(err) = (*system).run_unsafe(trigger, world) { diff --git a/crates/bevy_ecs/src/schedule/executor/mod.rs b/crates/bevy_ecs/src/schedule/executor/mod.rs index ef7c639038afe..cb6f3dcf8f111 100644 --- a/crates/bevy_ecs/src/schedule/executor/mod.rs +++ b/crates/bevy_ecs/src/schedule/executor/mod.rs @@ -203,6 +203,10 @@ impl System for ApplyDeferred { Ok(()) } + #[cfg(feature = "hotpatching")] + #[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. diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index e5fd94d2dde63..62a10298c9b83 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -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; @@ -443,6 +445,14 @@ impl ExecutorState { return; } + #[cfg(feature = "hotpatching")] + let should_update_hotpatch = !context + .environment + .world_cell + .get_resource::>() + .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); @@ -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, conditions) { // NOTE: exclusive systems with ambiguities are susceptible to // being significantly displaced here (compared to single-threaded order) diff --git a/crates/bevy_ecs/src/schedule/executor/simple.rs b/crates/bevy_ecs/src/schedule/executor/simple.rs index 584c5a1073046..d9069aa6e871d 100644 --- a/crates/bevy_ecs/src/schedule/executor/simple.rs +++ b/crates/bevy_ecs/src/schedule/executor/simple.rs @@ -16,6 +16,8 @@ use crate::{ }, world::World, }; +#[cfg(feature = "hotpatching")] +use crate::{event::Events, HotPatched}; use super::__rust_begin_short_backtrace; @@ -60,6 +62,12 @@ impl SystemExecutor for SimpleExecutor { self.completed_systems |= skipped_systems; } + #[cfg(feature = "hotpatching")] + let should_update_hotpatch = !world + .get_resource::>() + .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(); @@ -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); @@ -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::>() + .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." @@ -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) diff --git a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs index 0076103637778..68af623b408df 100644 --- a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs @@ -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; @@ -60,6 +62,12 @@ impl SystemExecutor for SingleThreadedExecutor { self.completed_systems |= skipped_systems; } + #[cfg(feature = "hotpatching")] + let should_update_hotpatch = !world + .get_resource::>() + .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(); @@ -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); @@ -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::>() + .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." @@ -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) diff --git a/crates/bevy_ecs/src/system/adapter_system.rs b/crates/bevy_ecs/src/system/adapter_system.rs index 50dbfad7ea39e..f728a4e9074b1 100644 --- a/crates/bevy_ecs/src/system/adapter_system.rs +++ b/crates/bevy_ecs/src/system/adapter_system.rs @@ -161,6 +161,12 @@ where }) } + #[cfg(feature = "hotpatching")] + #[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); diff --git a/crates/bevy_ecs/src/system/combinator.rs b/crates/bevy_ecs/src/system/combinator.rs index 0faade39ee307..29a87e93ceb34 100644 --- a/crates/bevy_ecs/src/system/combinator.rs +++ b/crates/bevy_ecs/src/system/combinator.rs @@ -182,6 +182,13 @@ where ) } + #[cfg(feature = "hotpatching")] + #[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); @@ -392,6 +399,13 @@ where self.b.run_unsafe(value, world) } + #[cfg(feature = "hotpatching")] + #[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); diff --git a/crates/bevy_ecs/src/system/exclusive_function_system.rs b/crates/bevy_ecs/src/system/exclusive_function_system.rs index 8277fcb0f9a80..0920fd1e1f163 100644 --- a/crates/bevy_ecs/src/system/exclusive_function_system.rs +++ b/crates/bevy_ecs/src/system/exclusive_function_system.rs @@ -26,6 +26,8 @@ where F: ExclusiveSystemParamFunction, { func: F, + #[cfg(feature = "hotpatching")] + current_ptr: subsecond::HotFnPtr, param_state: Option<::State>, system_meta: SystemMeta, // NOTE: PhantomData T> gives this safe Send/Sync impls @@ -58,6 +60,11 @@ where fn into_system(func: Self) -> Self::System { ExclusiveFunctionSystem { func, + #[cfg(feature = "hotpatching")] + current_ptr: subsecond::HotFn::current( + >::run, + ) + .ptr_address(), param_state: None, system_meta: SystemMeta::new::(), marker: PhantomData, @@ -125,6 +132,20 @@ where self.param_state.as_mut().expect(PARAM_MESSAGE), &self.system_meta, ); + + #[cfg(feature = "hotpatching")] + let out = { + let mut hot_fn = + subsecond::HotFn::current(>::run); + // SAFETY: + // - pointer used to call is from the current jump table + unsafe { + hot_fn + .try_call_with_ptr(self.current_ptr, (&mut self.func, world, input, params)) + .expect("Error calling hotpatched system. Run a full rebuild") + } + }; + #[cfg(not(feature = "hotpatching"))] let out = self.func.run(world, input, params); world.flush(); @@ -134,6 +155,17 @@ where }) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + let new = subsecond::HotFn::current(>::run) + .ptr_address(); + if new != self.current_ptr { + log::debug!("system {} hotpatched", self.name()); + } + self.current_ptr = new; + } + #[inline] fn apply_deferred(&mut self, _world: &mut World) { // "pure" exclusive systems do not have any buffers to apply. diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 49bcf1cc78845..af26e81d2f658 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -306,6 +306,9 @@ impl SystemState { ) -> FunctionSystem { FunctionSystem { func, + #[cfg(feature = "hotpatching")] + current_ptr: subsecond::HotFn::current(>::run) + .ptr_address(), state: Some(FunctionSystemState { param: self.param_state, world_id: self.world_id, @@ -519,6 +522,8 @@ where F: SystemParamFunction, { func: F, + #[cfg(feature = "hotpatching")] + current_ptr: subsecond::HotFnPtr, state: Option>, system_meta: SystemMeta, // NOTE: PhantomData T> gives this safe Send/Sync impls @@ -558,6 +563,9 @@ where fn clone(&self) -> Self { Self { func: self.func.clone(), + #[cfg(feature = "hotpatching")] + current_ptr: subsecond::HotFn::current(>::run) + .ptr_address(), state: None, system_meta: SystemMeta::new::(), marker: PhantomData, @@ -578,6 +586,9 @@ where fn into_system(func: Self) -> Self::System { FunctionSystem { func, + #[cfg(feature = "hotpatching")] + current_ptr: subsecond::HotFn::current(>::run) + .ptr_address(), state: None, system_meta: SystemMeta::new::(), marker: PhantomData, @@ -653,11 +664,35 @@ where // will ensure that there are no data access conflicts. let params = unsafe { F::Param::get_param(&mut state.param, &self.system_meta, world, change_tick) }; + + #[cfg(feature = "hotpatching")] + let out = { + let mut hot_fn = subsecond::HotFn::current(>::run); + // SAFETY: + // - pointer used to call is from the current jump table + unsafe { + hot_fn + .try_call_with_ptr(self.current_ptr, (&mut self.func, input, params)) + .expect("Error calling hotpatched system. Run a full rebuild") + } + }; + #[cfg(not(feature = "hotpatching"))] let out = self.func.run(input, params); + self.system_meta.last_run = change_tick; out } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + let new = subsecond::HotFn::current(>::run).ptr_address(); + if new != self.current_ptr { + log::debug!("system {} hotpatched", self.name()); + } + self.current_ptr = new; + } + #[inline] fn apply_deferred(&mut self, world: &mut World) { let param_state = &mut self.state.as_mut().expect(Self::ERROR_UNINITIALIZED).param; diff --git a/crates/bevy_ecs/src/system/observer_system.rs b/crates/bevy_ecs/src/system/observer_system.rs index d3138151c9750..c0ac5e8de094c 100644 --- a/crates/bevy_ecs/src/system/observer_system.rs +++ b/crates/bevy_ecs/src/system/observer_system.rs @@ -151,6 +151,12 @@ where Ok(()) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + self.observer.refresh_hotpatch(); + } + #[inline] fn apply_deferred(&mut self, world: &mut World) { self.observer.apply_deferred(world); diff --git a/crates/bevy_ecs/src/system/schedule_system.rs b/crates/bevy_ecs/src/system/schedule_system.rs index 962ff94a2a97e..26fdbdbe0f013 100644 --- a/crates/bevy_ecs/src/system/schedule_system.rs +++ b/crates/bevy_ecs/src/system/schedule_system.rs @@ -68,6 +68,12 @@ impl> System for InfallibleSystemWrapper { Ok(()) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + self.0.refresh_hotpatch(); + } + #[inline] fn apply_deferred(&mut self, world: &mut World) { self.0.apply_deferred(world); @@ -186,6 +192,12 @@ where self.system.run_unsafe(&mut self.value, world) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + self.system.refresh_hotpatch(); + } + fn apply_deferred(&mut self, world: &mut World) { self.system.apply_deferred(world); } @@ -293,6 +305,12 @@ where self.system.run_unsafe(value, world) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + self.system.refresh_hotpatch(); + } + fn apply_deferred(&mut self, world: &mut World) { self.system.apply_deferred(world); } diff --git a/crates/bevy_ecs/src/system/system.rs b/crates/bevy_ecs/src/system/system.rs index be650588bdeb0..408a0589fd775 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -76,6 +76,10 @@ pub trait System: Send + Sync + 'static { unsafe fn run_unsafe(&mut self, input: SystemIn<'_, Self>, world: UnsafeWorldCell) -> Self::Out; + /// Refresh the inner pointer based on the latest hot patch jump table + #[cfg(feature = "hotpatching")] + fn refresh_hotpatch(&mut self); + /// Runs the system with the given input in the world. /// /// For [read-only](ReadOnlySystem) systems, see [`run_readonly`], which can be called using `&World`. diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 819b8dab8e7bd..4547f55928e48 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -344,6 +344,8 @@ web = [ "bevy_tasks/web", ] +hotpatching = ["bevy_app/hotpatching", "bevy_ecs/hotpatching"] + [dependencies] # bevy (no_std) bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index db1152a362e31..5e94a7e6530dd 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -66,6 +66,8 @@ plugin_group! { bevy_dev_tools:::DevToolsPlugin, #[cfg(feature = "bevy_ci_testing")] bevy_dev_tools::ci_testing:::CiTestingPlugin, + #[cfg(feature = "hotpatching")] + bevy_app::hotpatch:::HotPatchPlugin, #[plugin_group] #[cfg(feature = "bevy_picking")] bevy_picking:::DefaultPickingPlugins, diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 1a1cb68fda2e1..e0f00f2f3de27 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -85,6 +85,7 @@ The default feature set enables most of the expected features of a game engine, |ghost_nodes|Experimental support for nodes that are ignored for UI layouting| |gif|GIF image format support| |glam_assert|Enable assertions to check the validity of parameters passed to glam| +|hotpatching|Enable hotpatching of Bevy systems| |ico|ICO image format support| |jpeg|JPEG image format support| |libm|Uses the `libm` maths library instead of the one provided in `std` and `core`.| diff --git a/examples/README.md b/examples/README.md index 4f5030563aa7d..d40160702f2f5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -318,6 +318,7 @@ Example | Description [Fixed Timestep](../examples/ecs/fixed_timestep.rs) | Shows how to create systems that run every fixed timestep, rather than every tick [Generic System](../examples/ecs/generic_system.rs) | Shows how to create systems that can be reused with different types [Hierarchy](../examples/ecs/hierarchy.rs) | Creates a hierarchy of parents and children entities +[Hotpatching Systems](../examples/ecs/hotpatching_systems.rs) | Demonstrates how to hotpatch systems [Immutable Components](../examples/ecs/immutable_components.rs) | Demonstrates the creation and utility of immutable components [Iter Combinations](../examples/ecs/iter_combinations.rs) | Shows how to iterate over combinations of query results [Nondeterministic System Order](../examples/ecs/nondeterministic_system_order.rs) | Systems run in parallel, but their order isn't always deterministic. Here's how to detect and fix this. diff --git a/examples/ecs/hotpatching_systems.rs b/examples/ecs/hotpatching_systems.rs new file mode 100644 index 0000000000000..0a6b94bf8a57c --- /dev/null +++ b/examples/ecs/hotpatching_systems.rs @@ -0,0 +1,94 @@ +//! This example demonstrates how to hot patch systems. +//! +//! It needs to be run with the dioxus CLI: +//! ```sh +//! dx serve --hot-patch --example hotpatching_systems --features hotpatching +//! ``` +//! +//! All systems are automatically hot patchable. +//! +//! You can change the text in the `update_text` system, or the color in the +//! `on_click` system, and those changes will be hotpatched into the running +//! application. +//! +//! It's also possible to make any function hot patchable by wrapping it with +//! `bevy::dev_tools::hotpatch::call`. + +use std::time::Duration; + +use bevy::{color::palettes, prelude::*}; + +fn main() { + let (sender, receiver) = crossbeam_channel::unbounded::<()>(); + + // This function is here to demonstrate how to make something hot patchable outside of a system + // It uses a thread for simplicity but could be an async task, an asset loader, ... + start_thread(receiver); + + App::new() + .add_plugins(DefaultPlugins) + .insert_resource(TaskSender(sender)) + .add_systems(Startup, setup) + .add_systems(Update, update_text) + .run(); +} + +fn update_text(mut text: Single<&mut Text>) { + // Anything in the body of a system can be changed. + // Changes to this string should be immediately visible in the example. + text.0 = "before".to_string(); +} + +fn on_click( + _click: Trigger>, + mut color: Single<&mut TextColor>, + task_sender: Res, +) { + // Observers are also hot patchable. + // If you change this color and click on the text in the example, it will have the new color. + color.0 = palettes::tailwind::RED_600.into(); + + let _ = task_sender.0.send(()); +} + +#[derive(Resource)] +struct TaskSender(crossbeam_channel::Sender<()>); + +fn setup(mut commands: Commands) { + commands.spawn(Camera2d); + + commands + .spawn(( + Node { + width: Val::Percent(100.0), + height: Val::Percent(100.0), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + flex_direction: FlexDirection::Column, + ..default() + }, + children![( + Text::default(), + TextFont { + font_size: 100.0, + ..default() + }, + )], + )) + .observe(on_click); +} + +fn start_thread(receiver: crossbeam_channel::Receiver<()>) { + std::thread::spawn(move || { + while receiver.recv().is_ok() { + let start = bevy::platform::time::Instant::now(); + + // You can also make any part outside of a system hot patchable by wrapping it + // In this part, only the duration is hot patchable: + let duration = bevy::app::hotpatch::call(|| Duration::from_secs(2)); + + std::thread::sleep(duration); + info!("done after {:?}", start.elapsed()); + } + }); +} diff --git a/release-content/release-notes/hot_patching.md b/release-content/release-notes/hot_patching.md new file mode 100644 index 0000000000000..bcbc085c40d01 --- /dev/null +++ b/release-content/release-notes/hot_patching.md @@ -0,0 +1,21 @@ +--- +title: Hot Patching Systems in a Running App +authors: ["@mockersf"] +pull_requests: [19309] +--- + +Bevy now supports hot patching systems through subsecond from the Dixous project. + +Enabled with the feature `hotpatching`, every system can now be modified during execution, and the change directly visible in your game. + +Run `BEVY_ASSET_ROOT="." dx serve --hot-patch --example hotpatching_systems --features hotpatching` to test it. + +`dx` is the Dioxus CLI, to install it run `cargo install dioxus-cli@0.7.0-alpha.1` +TODO: use the fixed version that will match the version of subsecond dependency used in Bevy at release time + +Known limitations: + +- Only works on the binary crate (todo: plan to support it in Dioxus) +- Not supported in Wasm (todo: supported in Dioxus but not yet implemented in Bevy) +- No system signature change support (todo: add that in Bevy) +- May be sensitive to rust/linker configuration (todo: better support in Dioxus)