From 06d9d6c6b43af124c9bebc6b12aa6cef2b0166c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Tue, 20 May 2025 08:37:26 +0200 Subject: [PATCH 01/18] easy hot-patching --- crates/bevy_ecs/Cargo.toml | 1 + crates/bevy_ecs/src/system/function_system.rs | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index 28987f1413449..c070ae210dc03 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -124,6 +124,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 = "0.7.0-alpha.0" 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/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 5cf3fe2a44cd6..009c6c813a820 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -737,7 +737,19 @@ where // will ensure that there are no data access conflicts. let params = unsafe { F::Param::get_param(param_state, &self.system_meta, world, change_tick) }; - let out = self.func.run(input, params); + + // SAFETY: + // - this is not safe + // - no way + // - nu uh + // - not even marked as unsafe, that's how unsafe this is + // - enjoy + let out = subsecond::HotFn::current(>::run).call(( + &mut self.func, + input, + params, + )); + self.system_meta.last_run = change_tick; out } From ca7d5cf034406dbbbf8ac0ffe82d9fafff3a74b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Tue, 20 May 2025 12:42:58 +0200 Subject: [PATCH 02/18] checking jump table only when needed --- Cargo.toml | 15 ++++++ crates/bevy_app/Cargo.toml | 8 +++ crates/bevy_app/src/hotpatch.rs | 32 +++++++++++ crates/bevy_app/src/lib.rs | 10 +++- crates/bevy_ecs/Cargo.toml | 4 +- crates/bevy_ecs/src/lib.rs | 10 ++++ crates/bevy_ecs/src/observer/runner.rs | 7 ++- crates/bevy_ecs/src/schedule/executor/mod.rs | 3 ++ .../src/schedule/executor/multi_threaded.rs | 15 ++++++ .../bevy_ecs/src/schedule/executor/simple.rs | 23 ++++++++ .../src/schedule/executor/single_threaded.rs | 23 ++++++++ crates/bevy_ecs/src/system/adapter_system.rs | 5 ++ crates/bevy_ecs/src/system/combinator.rs | 12 +++++ .../src/system/exclusive_function_system.rs | 5 ++ crates/bevy_ecs/src/system/function_system.rs | 32 +++++++++-- crates/bevy_ecs/src/system/observer_system.rs | 5 ++ crates/bevy_ecs/src/system/schedule_system.rs | 15 ++++++ crates/bevy_ecs/src/system/system.rs | 3 ++ crates/bevy_internal/Cargo.toml | 2 + crates/bevy_internal/src/default_plugins.rs | 2 + examples/ecs/hotpatching_systems.rs | 54 +++++++++++++++++++ 21 files changed, 277 insertions(+), 8 deletions(-) create mode 100644 crates/bevy_app/src/hotpatch.rs create mode 100644 examples/ecs/hotpatching_systems.rs diff --git a/Cargo.toml b/Cargo.toml index 7cf1334e81356..7a59c882502c3 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 } @@ -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" +wasm = false diff --git a/crates/bevy_app/Cargo.toml b/crates/bevy_app/Cargo.toml index f46db94db36bc..7f21d6a8b0778 100644 --- a/crates/bevy_app/Cargo.toml +++ b/crates/bevy_app/Cargo.toml @@ -72,6 +72,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" } @@ -90,6 +96,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 = { git = "https://github.com/mockersf/dioxus", branch = "typed-hotfn-pointer", 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..a6a599401ee9b --- /dev/null +++ b/crates/bevy_app/src/hotpatch.rs @@ -0,0 +1,32 @@ +use bevy_ecs::{event::EventWriter, HotPatched}; +use dioxus_devtools::{connect, subsecond::apply_patch, DevserverMsg}; + +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); + + connect(move |msg| { + if let DevserverMsg::HotReload(hot_reload_msg) = msg { + if let Some(jumptable) = hot_reload_msg.jump_table { + unsafe { apply_patch(jumptable).unwrap() }; + sender.send(HotPatched).unwrap(); + } + } + }); + + 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..220eaa47aee16 100644 --- a/crates/bevy_app/src/lib.rs +++ b/crates/bevy_app/src/lib.rs @@ -6,7 +6,11 @@ ) )] #![cfg_attr(any(docsrs, docsrs_dep), feature(doc_auto_cfg, rustdoc_internals))] -#![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" @@ -24,6 +28,8 @@ extern crate alloc; extern crate self as bevy_app; mod app; +#[cfg(feature = "hotpatching")] +mod hotpatch; mod main_schedule; mod panic_handler; mod plugin; @@ -35,6 +41,8 @@ mod task_pool_plugin; mod terminal_ctrl_c_handler; pub use app::*; +#[cfg(feature = "hotpatching")] +pub use hotpatch::HotPatchPlugin; pub use main_schedule::*; pub use panic_handler::*; pub use plugin::*; diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index c070ae210dc03..feecfb9a90958 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -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 = [ @@ -124,7 +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 = "0.7.0-alpha.0" +subsecond = { git = "https://github.com/mockersf/dioxus", branch = "typed-hotfn-pointer", 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 0a2e1862ddbf1..e0591c33de301 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. @@ -2768,3 +2771,10 @@ mod tests { fn custom_clone(_source: &SourceComponent, _ctx: &mut ComponentCloneCtx) {} } } + +/// Event triggered when a hotpatch happens. +/// +/// Systems should refresh their inner pointers. +#[cfg(feature = "hotpatching")] +#[derive(Event, Default)] +pub struct HotPatched; diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index be7bc4ede2613..817912b37d3f4 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -366,12 +366,17 @@ fn observer_system_runner>( }; // 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(()) => { diff --git a/crates/bevy_ecs/src/schedule/executor/mod.rs b/crates/bevy_ecs/src/schedule/executor/mod.rs index a601284fb005a..e4b6711735358 100644 --- a/crates/bevy_ecs/src/schedule/executor/mod.rs +++ b/crates/bevy_ecs/src/schedule/executor/mod.rs @@ -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. diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index 763504eaec422..fc18276d3a0c3 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(|e| e.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, system, diff --git a/crates/bevy_ecs/src/schedule/executor/simple.rs b/crates/bevy_ecs/src/schedule/executor/simple.rs index 584c5a1073046..21326f8ddc50e 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(|e| e.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(|e| e.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..df1440cbb287a 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(|e| e.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(|e| e.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 5953a43d70736..7c37827fb2ed1 100644 --- a/crates/bevy_ecs/src/system/adapter_system.rs +++ b/crates/bevy_ecs/src/system/adapter_system.rs @@ -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); diff --git a/crates/bevy_ecs/src/system/combinator.rs b/crates/bevy_ecs/src/system/combinator.rs index 9d11de95258a6..31305d8a1c8fd 100644 --- a/crates/bevy_ecs/src/system/combinator.rs +++ b/crates/bevy_ecs/src/system/combinator.rs @@ -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); @@ -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); diff --git a/crates/bevy_ecs/src/system/exclusive_function_system.rs b/crates/bevy_ecs/src/system/exclusive_function_system.rs index 9107993f9542a..a23088d7853a4 100644 --- a/crates/bevy_ecs/src/system/exclusive_function_system.rs +++ b/crates/bevy_ecs/src/system/exclusive_function_system.rs @@ -140,6 +140,11 @@ where }) } + #[inline] + fn refresh_hotpatch(&mut self) { + // TODO: support exclusive systems + } + #[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 009c6c813a820..925f17a99eb50 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -358,6 +358,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, @@ -595,6 +598,8 @@ where F: SystemParamFunction, { func: F, + #[cfg(feature = "hotpatching")] + current_ptr: subsecond::HotFnPtr, state: Option>, system_meta: SystemMeta, archetype_generation: ArchetypeGeneration, @@ -635,6 +640,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::(), archetype_generation: ArchetypeGeneration::initial(), @@ -656,6 +664,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::(), archetype_generation: ArchetypeGeneration::initial(), @@ -744,16 +755,27 @@ where // - nu uh // - not even marked as unsafe, that's how unsafe this is // - enjoy - let out = subsecond::HotFn::current(>::run).call(( - &mut self.func, - input, - params, - )); + #[cfg(feature = "hotpatching")] + let out = subsecond::HotFn::current(>::run) + .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 } + #[inline] + #[cfg(feature = "hotpatching")] + fn refresh_hotpatch(&mut self) { + self.current_ptr = + subsecond::HotFn::current(>::run).ptr_address(); + } + #[inline] + #[cfg(not(feature = "hotpatching"))] + fn refresh_hotpatch(&mut self) {} + #[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 9bd35c53615ba..671731a0dd898 100644 --- a/crates/bevy_ecs/src/system/observer_system.rs +++ b/crates/bevy_ecs/src/system/observer_system.rs @@ -157,6 +157,11 @@ where Ok(()) } + #[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 8ef7d9ed5752d..27c63b6164918 100644 --- a/crates/bevy_ecs/src/system/schedule_system.rs +++ b/crates/bevy_ecs/src/system/schedule_system.rs @@ -70,6 +70,11 @@ impl> System for InfallibleSystemWrapper { Ok(()) } + #[inline] + fn refresh_hotpatch(&mut self) { + self.0.refresh_hotpatch(); + } + #[inline] fn apply_deferred(&mut self, world: &mut World) { self.0.apply_deferred(world); @@ -197,6 +202,11 @@ where self.system.run_unsafe(&mut self.value, world) } + #[inline] + fn refresh_hotpatch(&mut self) { + self.system.refresh_hotpatch(); + } + fn apply_deferred(&mut self, world: &mut World) { self.system.apply_deferred(world); } @@ -312,6 +322,11 @@ where self.system.run_unsafe(value, world) } + #[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 18ec7f44cd4ff..d3a8a8e5b9b12 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -82,6 +82,9 @@ 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 + 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 38f89d4ef8efc..acc9a95fd3b92 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -346,6 +346,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..adcfbc2d7211c 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -4,6 +4,8 @@ plugin_group! { /// This plugin group will add all the default plugins for a *Bevy* application: pub struct DefaultPlugins { bevy_app:::PanicHandlerPlugin, + #[cfg(feature = "hotpatching")] + bevy_app:::HotPatchPlugin, #[cfg(feature = "bevy_log")] bevy_log:::LogPlugin, bevy_app:::TaskPoolPlugin, diff --git a/examples/ecs/hotpatching_systems.rs b/examples/ecs/hotpatching_systems.rs new file mode 100644 index 0000000000000..1d7d5fb8593d1 --- /dev/null +++ b/examples/ecs/hotpatching_systems.rs @@ -0,0 +1,54 @@ +//! This example demonstrates how to hotpatch systems. +//! +//! It needs to be run with the dioxus CLI: +//! ```sh +//! dx serve --hot-patch --example hotpatching_systems --features hotpatching +//! ``` +//! +//! 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. + +use bevy::{color::palettes, prelude::*}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, update_text) + .run(); +} + +fn update_text(mut text_query: Query<&mut Text>) { + let mut text = text_query.single_mut().unwrap(); + **text = "before".to_string(); +} + +fn on_click(_click: Trigger>, mut color: Query<&mut TextColor>) -> Result { + color.single_mut()?.0 = palettes::tailwind::RED_600.into(); + Ok(()) +} + +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); +} From 6eeae7fde5dbb60d981144fcb81cd7dad8c1e15d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Tue, 20 May 2025 12:55:06 +0200 Subject: [PATCH 03/18] clippy --- crates/bevy_ecs/src/schedule/executor/multi_threaded.rs | 2 +- crates/bevy_ecs/src/schedule/executor/simple.rs | 4 ++-- crates/bevy_ecs/src/schedule/executor/single_threaded.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index fc18276d3a0c3..dd0a930a46afa 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -450,7 +450,7 @@ impl ExecutorState { .environment .world_cell .get_resource::>() - .map(|e| e.is_empty()) + .map(Events::is_empty) .unwrap_or(true); // can't borrow since loop mutably borrows `self` diff --git a/crates/bevy_ecs/src/schedule/executor/simple.rs b/crates/bevy_ecs/src/schedule/executor/simple.rs index 21326f8ddc50e..d9069aa6e871d 100644 --- a/crates/bevy_ecs/src/schedule/executor/simple.rs +++ b/crates/bevy_ecs/src/schedule/executor/simple.rs @@ -65,7 +65,7 @@ impl SystemExecutor for SimpleExecutor { #[cfg(feature = "hotpatching")] let should_update_hotpatch = !world .get_resource::>() - .map(|e| e.is_empty()) + .map(Events::is_empty) .unwrap_or(true); for system_index in 0..schedule.systems.len() { @@ -202,7 +202,7 @@ fn evaluate_and_fold_conditions( #[cfg(feature = "hotpatching")] let should_update_hotpatch = !world .get_resource::>() - .map(|e| e.is_empty()) + .map(Events::is_empty) .unwrap_or(true); #[expect( diff --git a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs index df1440cbb287a..68af623b408df 100644 --- a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs @@ -65,7 +65,7 @@ impl SystemExecutor for SingleThreadedExecutor { #[cfg(feature = "hotpatching")] let should_update_hotpatch = !world .get_resource::>() - .map(|e| e.is_empty()) + .map(Events::is_empty) .unwrap_or(true); for system_index in 0..schedule.systems.len() { @@ -220,7 +220,7 @@ fn evaluate_and_fold_conditions( #[cfg(feature = "hotpatching")] let should_update_hotpatch = !world .get_resource::>() - .map(|e| e.is_empty()) + .map(Events::is_empty) .unwrap_or(true); #[expect( From 5ac3e414da1538e67b384d0a33300eb68f56a6ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Tue, 20 May 2025 12:55:12 +0200 Subject: [PATCH 04/18] example readme --- Cargo.toml | 2 +- examples/README.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 7a59c882502c3..caea7347b8611 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4370,5 +4370,5 @@ required-features = ["hotpatching"] [package.metadata.example.hotpatching_systems] name = "Hotpatching Systems" description = "Demonstrates how to hotpatch systems" -category = "Ecs" +category = "ECS (Entity Component System)" wasm = false diff --git a/examples/README.md b/examples/README.md index 060683f96d891..185b825c75b75 100644 --- a/examples/README.md +++ b/examples/README.md @@ -317,6 +317,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. From dc635d89e79146a4848e0811d190d9d4bbf93cec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Tue, 20 May 2025 12:57:53 +0200 Subject: [PATCH 05/18] also feature doc --- docs/cargo_features.md | 1 + 1 file changed, 1 insertion(+) 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`.| From d92ae905ce8942560fb8c48b204d379ed6c01047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Tue, 20 May 2025 13:06:41 +0200 Subject: [PATCH 06/18] safety comment --- crates/bevy_app/src/hotpatch.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bevy_app/src/hotpatch.rs b/crates/bevy_app/src/hotpatch.rs index a6a599401ee9b..2ce1d49ec7d3a 100644 --- a/crates/bevy_app/src/hotpatch.rs +++ b/crates/bevy_app/src/hotpatch.rs @@ -14,6 +14,8 @@ impl Plugin for HotPatchPlugin { connect(move |msg| { 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(); } From 3be68fad4bf39ec69df192bedfe4eee3e25bace2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Tue, 20 May 2025 13:47:09 +0200 Subject: [PATCH 07/18] nicer example systems Co-authored-by: Jan Hohenheim --- examples/ecs/hotpatching_systems.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/examples/ecs/hotpatching_systems.rs b/examples/ecs/hotpatching_systems.rs index 1d7d5fb8593d1..d1d95fa7dddf8 100644 --- a/examples/ecs/hotpatching_systems.rs +++ b/examples/ecs/hotpatching_systems.rs @@ -19,14 +19,12 @@ fn main() { .run(); } -fn update_text(mut text_query: Query<&mut Text>) { - let mut text = text_query.single_mut().unwrap(); +fn update_text(mut tex: Single<&mut Text>) { **text = "before".to_string(); } -fn on_click(_click: Trigger>, mut color: Query<&mut TextColor>) -> Result { - color.single_mut()?.0 = palettes::tailwind::RED_600.into(); - Ok(()) +fn on_click(_click: Trigger>, mut color: Single<&mut TextColor>) { + color.0 = palettes::tailwind::RED_600.into(); } fn setup(mut commands: Commands) { From d4290fb29c190d4bb9473b01157c51f3430b1cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Tue, 20 May 2025 13:51:34 +0200 Subject: [PATCH 08/18] fix review --- examples/ecs/hotpatching_systems.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ecs/hotpatching_systems.rs b/examples/ecs/hotpatching_systems.rs index d1d95fa7dddf8..c7fac9d4cf95e 100644 --- a/examples/ecs/hotpatching_systems.rs +++ b/examples/ecs/hotpatching_systems.rs @@ -19,8 +19,8 @@ fn main() { .run(); } -fn update_text(mut tex: Single<&mut Text>) { - **text = "before".to_string(); +fn update_text(mut text: Single<&mut Text>) { + text.0 = "before".to_string(); } fn on_click(_click: Trigger>, mut color: Single<&mut TextColor>) { From 21ff0deaf56c7c703d4745777145a5e49af08e39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Tue, 20 May 2025 21:55:51 +0200 Subject: [PATCH 09/18] move hotpatch plugin to bevy_dev_tools --- crates/bevy_app/Cargo.toml | 8 -------- crates/bevy_app/src/lib.rs | 10 +--------- crates/bevy_dev_tools/Cargo.toml | 8 ++++++++ crates/{bevy_app => bevy_dev_tools}/src/hotpatch.rs | 2 ++ crates/bevy_dev_tools/src/lib.rs | 9 ++++++++- crates/bevy_internal/Cargo.toml | 2 +- crates/bevy_internal/src/default_plugins.rs | 4 ++-- 7 files changed, 22 insertions(+), 21 deletions(-) rename crates/{bevy_app => bevy_dev_tools}/src/hotpatch.rs (96%) diff --git a/crates/bevy_app/Cargo.toml b/crates/bevy_app/Cargo.toml index 7f21d6a8b0778..f46db94db36bc 100644 --- a/crates/bevy_app/Cargo.toml +++ b/crates/bevy_app/Cargo.toml @@ -72,12 +72,6 @@ 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" } @@ -96,8 +90,6 @@ 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 = { git = "https://github.com/mockersf/dioxus", branch = "typed-hotfn-pointer", 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/lib.rs b/crates/bevy_app/src/lib.rs index 220eaa47aee16..743806df71b6f 100644 --- a/crates/bevy_app/src/lib.rs +++ b/crates/bevy_app/src/lib.rs @@ -6,11 +6,7 @@ ) )] #![cfg_attr(any(docsrs, docsrs_dep), feature(doc_auto_cfg, rustdoc_internals))] -#![cfg_attr( - feature = "hotpatching", - expect(unsafe_code, reason = "Unsafe code for system hotpatching.") -)] -#![cfg_attr(not(feature = "hotpatching"), forbid(unsafe_code))] +#![forbid(unsafe_code)] #![doc( html_logo_url = "https://bevyengine.org/assets/icon.png", html_favicon_url = "https://bevyengine.org/assets/icon.png" @@ -28,8 +24,6 @@ extern crate alloc; extern crate self as bevy_app; mod app; -#[cfg(feature = "hotpatching")] -mod hotpatch; mod main_schedule; mod panic_handler; mod plugin; @@ -41,8 +35,6 @@ mod task_pool_plugin; mod terminal_ctrl_c_handler; pub use app::*; -#[cfg(feature = "hotpatching")] -pub use hotpatch::HotPatchPlugin; pub use main_schedule::*; pub use panic_handler::*; pub use plugin::*; diff --git a/crates/bevy_dev_tools/Cargo.toml b/crates/bevy_dev_tools/Cargo.toml index ad0f2c515ca26..32a6ff75e9388 100644 --- a/crates/bevy_dev_tools/Cargo.toml +++ b/crates/bevy_dev_tools/Cargo.toml @@ -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" } @@ -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/mockersf/dioxus", branch = "typed-hotfn-pointer", optional = true } +crossbeam-channel = { version = "0.5.0", optional = true } [lints] workspace = true diff --git a/crates/bevy_app/src/hotpatch.rs b/crates/bevy_dev_tools/src/hotpatch.rs similarity index 96% rename from crates/bevy_app/src/hotpatch.rs rename to crates/bevy_dev_tools/src/hotpatch.rs index 2ce1d49ec7d3a..90910de066bb1 100644 --- a/crates/bevy_app/src/hotpatch.rs +++ b/crates/bevy_dev_tools/src/hotpatch.rs @@ -1,3 +1,5 @@ +//! Utilities for hotpatching code. + use bevy_ecs::{event::EventWriter, HotPatched}; use dioxus_devtools::{connect, subsecond::apply_patch, DevserverMsg}; diff --git a/crates/bevy_dev_tools/src/lib.rs b/crates/bevy_dev_tools/src/lib.rs index 1dfd4734090cc..f6bd9b0b8c24e 100644 --- a/crates/bevy_dev_tools/src/lib.rs +++ b/crates/bevy_dev_tools/src/lib.rs @@ -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" @@ -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; diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index acc9a95fd3b92..10d0be8771947 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -346,7 +346,7 @@ web = [ "bevy_tasks/web", ] -hotpatching = ["bevy_app/hotpatching", "bevy_ecs/hotpatching"] +hotpatching = ["bevy_dev_tools/hotpatching", "bevy_ecs/hotpatching"] [dependencies] # bevy (no_std) diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index adcfbc2d7211c..b5b55bf386404 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -4,8 +4,6 @@ plugin_group! { /// This plugin group will add all the default plugins for a *Bevy* application: pub struct DefaultPlugins { bevy_app:::PanicHandlerPlugin, - #[cfg(feature = "hotpatching")] - bevy_app:::HotPatchPlugin, #[cfg(feature = "bevy_log")] bevy_log:::LogPlugin, bevy_app:::TaskPoolPlugin, @@ -68,6 +66,8 @@ plugin_group! { bevy_dev_tools:::DevToolsPlugin, #[cfg(feature = "bevy_ci_testing")] bevy_dev_tools::ci_testing:::CiTestingPlugin, + #[cfg(feature = "hotpatching")] + bevy_dev_tools::hotpatch:::HotPatchPlugin, #[plugin_group] #[cfg(feature = "bevy_picking")] bevy_picking:::DefaultPickingPlugins, From 9c73e08ef46291a5a01a814119787e1c0d904c15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Tue, 20 May 2025 22:10:35 +0200 Subject: [PATCH 10/18] log when a system is hot-reloaded --- crates/bevy_ecs/src/system/function_system.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 925f17a99eb50..b90564eae27fe 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -769,8 +769,11 @@ where #[inline] #[cfg(feature = "hotpatching")] fn refresh_hotpatch(&mut self) { - self.current_ptr = - subsecond::HotFn::current(>::run).ptr_address(); + let new = subsecond::HotFn::current(>::run).ptr_address(); + if new != self.current_ptr { + log::info!("system {} hotpatched", self.name()); + } + self.current_ptr = new; } #[inline] #[cfg(not(feature = "hotpatching"))] From 6f6490bd4f17ba431f430b472b0ff8e105a300c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Tue, 20 May 2025 22:38:03 +0200 Subject: [PATCH 11/18] example on how to patch something out of the ecs --- crates/bevy_dev_tools/src/hotpatch.rs | 2 ++ crates/bevy_internal/Cargo.toml | 2 +- examples/ecs/hotpatching_systems.rs | 27 ++++++++++++++++++++++++++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/crates/bevy_dev_tools/src/hotpatch.rs b/crates/bevy_dev_tools/src/hotpatch.rs index 90910de066bb1..9eea581ed3026 100644 --- a/crates/bevy_dev_tools/src/hotpatch.rs +++ b/crates/bevy_dev_tools/src/hotpatch.rs @@ -3,6 +3,8 @@ 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. diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 10d0be8771947..9f31c95be07f7 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -346,7 +346,7 @@ web = [ "bevy_tasks/web", ] -hotpatching = ["bevy_dev_tools/hotpatching", "bevy_ecs/hotpatching"] +hotpatching = ["bevy_dev_tools", "bevy_dev_tools/hotpatching", "bevy_ecs/hotpatching"] [dependencies] # bevy (no_std) diff --git a/examples/ecs/hotpatching_systems.rs b/examples/ecs/hotpatching_systems.rs index c7fac9d4cf95e..a1c72bc3a9362 100644 --- a/examples/ecs/hotpatching_systems.rs +++ b/examples/ecs/hotpatching_systems.rs @@ -9,11 +9,27 @@ //! `on_click` system, and those changes will be hotpatched into the running //! application. +use std::time::Duration; + use bevy::{color::palettes, prelude::*}; fn main() { + let (sender, receiver) = crossbeam_channel::unbounded::<()>(); + + 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::dev_tools::hotpatch::call(|| Duration::from_secs(2)); + std::thread::sleep(duration); + info!("done after {:?}", start.elapsed()); + } + }); + App::new() .add_plugins(DefaultPlugins) + .insert_resource(TaskSender(sender)) .add_systems(Startup, setup) .add_systems(Update, update_text) .run(); @@ -23,10 +39,19 @@ fn update_text(mut text: Single<&mut Text>) { text.0 = "before".to_string(); } -fn on_click(_click: Trigger>, mut color: Single<&mut TextColor>) { +fn on_click( + _click: Trigger>, + mut color: Single<&mut TextColor>, + task_sender: Res, +) { 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); From 23dc91de42f533eb8fcd4849bac511fc0ca838ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Tue, 20 May 2025 22:38:31 +0200 Subject: [PATCH 12/18] change log level --- crates/bevy_ecs/src/system/function_system.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index b90564eae27fe..4260e16fd68bc 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -771,7 +771,7 @@ where fn refresh_hotpatch(&mut self) { let new = subsecond::HotFn::current(>::run).ptr_address(); if new != self.current_ptr { - log::info!("system {} hotpatched", self.name()); + log::debug!("system {} hotpatched", self.name()); } self.current_ptr = new; } From 0e7ab181452add6b0c59751ecc0415027473c4a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Tue, 20 May 2025 22:47:40 +0200 Subject: [PATCH 13/18] formatting --- crates/bevy_internal/Cargo.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 9f31c95be07f7..3f3386a9d4fca 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -346,7 +346,11 @@ web = [ "bevy_tasks/web", ] -hotpatching = ["bevy_dev_tools", "bevy_dev_tools/hotpatching", "bevy_ecs/hotpatching"] +hotpatching = [ + "bevy_dev_tools", + "bevy_dev_tools/hotpatching", + "bevy_ecs/hotpatching", +] [dependencies] # bevy (no_std) From 5ced02032ff1e07a02a975db47f36329b34604e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Tue, 20 May 2025 22:54:57 +0200 Subject: [PATCH 14/18] comments --- crates/bevy_dev_tools/src/hotpatch.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/bevy_dev_tools/src/hotpatch.rs b/crates/bevy_dev_tools/src/hotpatch.rs index 9eea581ed3026..c233f670c7c6c 100644 --- a/crates/bevy_dev_tools/src/hotpatch.rs +++ b/crates/bevy_dev_tools/src/hotpatch.rs @@ -15,6 +15,9 @@ 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 + // On a successful rebuild it sends a `HotReload` message with the new jump table + // When receiving that message, update the table and sends a `HotPatched` message through the channel connect(move |msg| { if let DevserverMsg::HotReload(hot_reload_msg) = msg { if let Some(jumptable) = hot_reload_msg.jump_table { @@ -26,6 +29,7 @@ impl Plugin for HotPatchPlugin { } }); + // 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| { From 27acbd747d5bdf75993f6c2886ab17b3331b4725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Tue, 20 May 2025 23:05:09 +0200 Subject: [PATCH 15/18] example improvements --- examples/ecs/hotpatching_systems.rs | 39 +++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/examples/ecs/hotpatching_systems.rs b/examples/ecs/hotpatching_systems.rs index a1c72bc3a9362..eda2eebe00d50 100644 --- a/examples/ecs/hotpatching_systems.rs +++ b/examples/ecs/hotpatching_systems.rs @@ -1,13 +1,18 @@ -//! This example demonstrates how to hotpatch systems. +//! 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; @@ -16,16 +21,9 @@ use bevy::{color::palettes, prelude::*}; fn main() { let (sender, receiver) = crossbeam_channel::unbounded::<()>(); - 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::dev_tools::hotpatch::call(|| Duration::from_secs(2)); - std::thread::sleep(duration); - info!("done after {:?}", start.elapsed()); - } - }); + // 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) @@ -36,6 +34,8 @@ fn main() { } 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(); } @@ -44,6 +44,8 @@ fn on_click( 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(()); @@ -75,3 +77,18 @@ fn setup(mut commands: Commands) { )) .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::dev_tools::hotpatch::call(|| Duration::from_secs(2)); + + std::thread::sleep(duration); + info!("done after {:?}", start.elapsed()); + } + }); +} From 5a540a16f34a70fccec26e103911293852355d32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Tue, 20 May 2025 23:25:21 +0200 Subject: [PATCH 16/18] Apply suggestions from code review Co-authored-by: JMS55 <47158642+JMS55@users.noreply.github.com> --- crates/bevy_dev_tools/src/hotpatch.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_dev_tools/src/hotpatch.rs b/crates/bevy_dev_tools/src/hotpatch.rs index c233f670c7c6c..991b0685025b2 100644 --- a/crates/bevy_dev_tools/src/hotpatch.rs +++ b/crates/bevy_dev_tools/src/hotpatch.rs @@ -16,8 +16,8 @@ impl Plugin for HotPatchPlugin { let (sender, receiver) = crossbeam_channel::bounded::(1); // Connects to the dioxus CLI that will handle rebuilds - // On a successful rebuild it sends a `HotReload` message with the new jump table - // When receiving that message, update the table and sends a `HotPatched` message through the channel + // 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| { if let DevserverMsg::HotReload(hot_reload_msg) = msg { if let Some(jumptable) = hot_reload_msg.jump_table { From a60862ae20ea33fe63c02095840373700656c4d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Wed, 21 May 2025 03:26:11 +0200 Subject: [PATCH 17/18] Update crates/bevy_ecs/src/lib.rs Co-authored-by: Jan Hohenheim --- crates/bevy_ecs/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index e0591c33de301..2e23fa50fb2f6 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -2772,7 +2772,7 @@ mod tests { } } -/// Event triggered when a hotpatch happens. +/// Event sent when a hotpatch happens. /// /// Systems should refresh their inner pointers. #[cfg(feature = "hotpatching")] From 04157b891ee15d632937b01fb2debd5bc4bc6707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Thu, 22 May 2025 22:17:53 +0200 Subject: [PATCH 18/18] use main from Dioxus --- crates/bevy_dev_tools/Cargo.toml | 2 +- crates/bevy_ecs/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_dev_tools/Cargo.toml b/crates/bevy_dev_tools/Cargo.toml index 32a6ff75e9388..497b06bf6df1c 100644 --- a/crates/bevy_dev_tools/Cargo.toml +++ b/crates/bevy_dev_tools/Cargo.toml @@ -39,7 +39,7 @@ 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/mockersf/dioxus", branch = "typed-hotfn-pointer", optional = true } +dioxus-devtools = { git = "https://github.com/DioxusLabs/dioxus", optional = true } crossbeam-channel = { version = "0.5.0", optional = true } [lints] diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index feecfb9a90958..d9c68f61139e4 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -126,7 +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/mockersf/dioxus", branch = "typed-hotfn-pointer", optional = true } +subsecond = { git = "https://github.com/DioxusLabs/dioxus", 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]