Hot patching systems with subsecond (#19309)
# Objective - Enable hot patching systems with subsecond - Fixes #19296 ## Solution - First commit is the naive thin layer - Second commit only check the jump table when the code is hot patched instead of on every system execution - Depends on https://github.com/DioxusLabs/dioxus/pull/4153 for a nicer API, but could be done without - Everything in second commit is feature gated, it has no impact when the feature is not enabled ## Testing - Check dependencies without the feature enabled: nothing dioxus in tree - Run the new example: text and color can be changed --------- Co-authored-by: Jan Hohenheim <jan@hohenheim.ch> Co-authored-by: JMS55 <47158642+JMS55@users.noreply.github.com>
This commit is contained in:
parent
50aa40e980
commit
7a7bff8c17
15
Cargo.toml
15
Cargo.toml
@ -534,6 +534,9 @@ libm = ["bevy_internal/libm"]
|
|||||||
# Enables use of browser APIs. Note this is currently only applicable on `wasm32` architectures.
|
# Enables use of browser APIs. Note this is currently only applicable on `wasm32` architectures.
|
||||||
web = ["bevy_internal/web"]
|
web = ["bevy_internal/web"]
|
||||||
|
|
||||||
|
# Enable hotpatching of Bevy systems
|
||||||
|
hotpatching = ["bevy_internal/hotpatching"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bevy_internal = { path = "crates/bevy_internal", version = "0.16.0-dev", default-features = false }
|
bevy_internal = { path = "crates/bevy_internal", version = "0.16.0-dev", default-features = false }
|
||||||
tracing = { version = "0.1", default-features = false, optional = true }
|
tracing = { version = "0.1", default-features = false, optional = true }
|
||||||
@ -4411,3 +4414,15 @@ name = "Cooldown"
|
|||||||
description = "Example for cooldown on button clicks"
|
description = "Example for cooldown on button clicks"
|
||||||
category = "Usage"
|
category = "Usage"
|
||||||
wasm = true
|
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
|
||||||
|
@ -71,6 +71,12 @@ web = [
|
|||||||
"dep:console_error_panic_hook",
|
"dep:console_error_panic_hook",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
hotpatching = [
|
||||||
|
"bevy_ecs/hotpatching",
|
||||||
|
"dep:dioxus-devtools",
|
||||||
|
"dep:crossbeam-channel",
|
||||||
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# bevy
|
# bevy
|
||||||
bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" }
|
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 }
|
tracing = { version = "0.1", default-features = false, optional = true }
|
||||||
log = { version = "0.4", default-features = false }
|
log = { version = "0.4", default-features = false }
|
||||||
cfg-if = "1.0.0"
|
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(all(unix, not(target_os = "horizon")), windows))'.dependencies]
|
[target.'cfg(any(all(unix, not(target_os = "horizon")), windows))'.dependencies]
|
||||||
ctrlc = { version = "3.4.4", optional = true }
|
ctrlc = { version = "3.4.4", optional = true }
|
||||||
|
42
crates/bevy_app/src/hotpatch.rs
Normal file
42
crates/bevy_app/src/hotpatch.rs
Normal file
@ -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::<HotPatched>(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::<HotPatched>().add_systems(
|
||||||
|
Last,
|
||||||
|
move |mut events: EventWriter<HotPatched>| {
|
||||||
|
if receiver.try_recv().is_ok() {
|
||||||
|
events.write_default();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -34,6 +34,9 @@ mod task_pool_plugin;
|
|||||||
#[cfg(all(any(all(unix, not(target_os = "horizon")), windows), feature = "std"))]
|
#[cfg(all(any(all(unix, not(target_os = "horizon")), windows), feature = "std"))]
|
||||||
mod terminal_ctrl_c_handler;
|
mod terminal_ctrl_c_handler;
|
||||||
|
|
||||||
|
#[cfg(feature = "hotpatching")]
|
||||||
|
pub mod hotpatch;
|
||||||
|
|
||||||
pub use app::*;
|
pub use app::*;
|
||||||
pub use main_schedule::*;
|
pub use main_schedule::*;
|
||||||
pub use panic_handler::*;
|
pub use panic_handler::*;
|
||||||
|
@ -83,6 +83,8 @@ critical-section = [
|
|||||||
"bevy_reflect?/critical-section",
|
"bevy_reflect?/critical-section",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
hotpatching = ["dep:subsecond"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bevy_ptr = { path = "../bevy_ptr", version = "0.16.0-dev" }
|
bevy_ptr = { path = "../bevy_ptr", version = "0.16.0-dev" }
|
||||||
bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [
|
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 }
|
tracing = { version = "0.1", default-features = false, optional = true }
|
||||||
log = { version = "0.4", default-features = false }
|
log = { version = "0.4", default-features = false }
|
||||||
bumpalo = "3"
|
bumpalo = "3"
|
||||||
|
subsecond = { version = "0.7.0-alpha.1", optional = true }
|
||||||
|
|
||||||
concurrent-queue = { version = "2.5.0", default-features = false }
|
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]
|
[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]
|
||||||
|
@ -59,6 +59,9 @@ pub mod world;
|
|||||||
|
|
||||||
pub use bevy_ptr as ptr;
|
pub use bevy_ptr as ptr;
|
||||||
|
|
||||||
|
#[cfg(feature = "hotpatching")]
|
||||||
|
use event::Event;
|
||||||
|
|
||||||
/// The ECS prelude.
|
/// The ECS prelude.
|
||||||
///
|
///
|
||||||
/// This includes the most common types in this crate, re-exported for your convenience.
|
/// 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;
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -371,6 +371,11 @@ fn observer_system_runner<E: Event, B: Bundle, S: ObserverSystem<E, B>>(
|
|||||||
// and is never exclusive
|
// and is never exclusive
|
||||||
// - system is the same type erased system from above
|
// - system is the same type erased system from above
|
||||||
unsafe {
|
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) {
|
match (*system).validate_param_unsafe(world) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
if let Err(err) = (*system).run_unsafe(trigger, world) {
|
if let Err(err) = (*system).run_unsafe(trigger, world) {
|
||||||
|
@ -203,6 +203,10 @@ impl System for ApplyDeferred {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "hotpatching")]
|
||||||
|
#[inline]
|
||||||
|
fn refresh_hotpatch(&mut self) {}
|
||||||
|
|
||||||
fn run(&mut self, _input: SystemIn<'_, Self>, _world: &mut World) -> Self::Out {
|
fn run(&mut self, _input: SystemIn<'_, Self>, _world: &mut World) -> Self::Out {
|
||||||
// This system does nothing on its own. The executor will apply deferred
|
// This system does nothing on its own. The executor will apply deferred
|
||||||
// commands from other systems instead of running this system.
|
// commands from other systems instead of running this system.
|
||||||
|
@ -19,6 +19,8 @@ use crate::{
|
|||||||
system::ScheduleSystem,
|
system::ScheduleSystem,
|
||||||
world::{unsafe_world_cell::UnsafeWorldCell, World},
|
world::{unsafe_world_cell::UnsafeWorldCell, World},
|
||||||
};
|
};
|
||||||
|
#[cfg(feature = "hotpatching")]
|
||||||
|
use crate::{event::Events, HotPatched};
|
||||||
|
|
||||||
use super::__rust_begin_short_backtrace;
|
use super::__rust_begin_short_backtrace;
|
||||||
|
|
||||||
@ -443,6 +445,14 @@ impl ExecutorState {
|
|||||||
return;
|
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`
|
// can't borrow since loop mutably borrows `self`
|
||||||
let mut ready_systems = core::mem::take(&mut self.ready_systems_copy);
|
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.
|
// Therefore, no other reference to this system exists and there is no aliasing.
|
||||||
let system = unsafe { &mut *context.environment.systems[system_index].get() };
|
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) {
|
if !self.can_run(system_index, conditions) {
|
||||||
// NOTE: exclusive systems with ambiguities are susceptible to
|
// NOTE: exclusive systems with ambiguities are susceptible to
|
||||||
// being significantly displaced here (compared to single-threaded order)
|
// being significantly displaced here (compared to single-threaded order)
|
||||||
|
@ -16,6 +16,8 @@ use crate::{
|
|||||||
},
|
},
|
||||||
world::World,
|
world::World,
|
||||||
};
|
};
|
||||||
|
#[cfg(feature = "hotpatching")]
|
||||||
|
use crate::{event::Events, HotPatched};
|
||||||
|
|
||||||
use super::__rust_begin_short_backtrace;
|
use super::__rust_begin_short_backtrace;
|
||||||
|
|
||||||
@ -60,6 +62,12 @@ impl SystemExecutor for SimpleExecutor {
|
|||||||
self.completed_systems |= skipped_systems;
|
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() {
|
for system_index in 0..schedule.systems.len() {
|
||||||
#[cfg(feature = "trace")]
|
#[cfg(feature = "trace")]
|
||||||
let name = schedule.systems[system_index].name();
|
let name = schedule.systems[system_index].name();
|
||||||
@ -120,6 +128,11 @@ impl SystemExecutor for SimpleExecutor {
|
|||||||
#[cfg(feature = "trace")]
|
#[cfg(feature = "trace")]
|
||||||
should_run_span.exit();
|
should_run_span.exit();
|
||||||
|
|
||||||
|
#[cfg(feature = "hotpatching")]
|
||||||
|
if should_update_hotpatch {
|
||||||
|
system.refresh_hotpatch();
|
||||||
|
}
|
||||||
|
|
||||||
// system has either been skipped or will run
|
// system has either been skipped or will run
|
||||||
self.completed_systems.insert(system_index);
|
self.completed_systems.insert(system_index);
|
||||||
|
|
||||||
@ -186,6 +199,12 @@ fn evaluate_and_fold_conditions(
|
|||||||
world: &mut World,
|
world: &mut World,
|
||||||
error_handler: ErrorHandler,
|
error_handler: ErrorHandler,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
|
#[cfg(feature = "hotpatching")]
|
||||||
|
let should_update_hotpatch = !world
|
||||||
|
.get_resource::<Events<HotPatched>>()
|
||||||
|
.map(Events::is_empty)
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
#[expect(
|
#[expect(
|
||||||
clippy::unnecessary_fold,
|
clippy::unnecessary_fold,
|
||||||
reason = "Short-circuiting here would prevent conditions from mutating their own state as needed."
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "hotpatching")]
|
||||||
|
if should_update_hotpatch {
|
||||||
|
condition.refresh_hotpatch();
|
||||||
|
}
|
||||||
__rust_begin_short_backtrace::readonly_run(&mut **condition, world)
|
__rust_begin_short_backtrace::readonly_run(&mut **condition, world)
|
||||||
})
|
})
|
||||||
.fold(true, |acc, res| acc && res)
|
.fold(true, |acc, res| acc && res)
|
||||||
|
@ -12,6 +12,8 @@ use crate::{
|
|||||||
schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule},
|
schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule},
|
||||||
world::World,
|
world::World,
|
||||||
};
|
};
|
||||||
|
#[cfg(feature = "hotpatching")]
|
||||||
|
use crate::{event::Events, HotPatched};
|
||||||
|
|
||||||
use super::__rust_begin_short_backtrace;
|
use super::__rust_begin_short_backtrace;
|
||||||
|
|
||||||
@ -60,6 +62,12 @@ impl SystemExecutor for SingleThreadedExecutor {
|
|||||||
self.completed_systems |= skipped_systems;
|
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() {
|
for system_index in 0..schedule.systems.len() {
|
||||||
#[cfg(feature = "trace")]
|
#[cfg(feature = "trace")]
|
||||||
let name = schedule.systems[system_index].name();
|
let name = schedule.systems[system_index].name();
|
||||||
@ -121,6 +129,11 @@ impl SystemExecutor for SingleThreadedExecutor {
|
|||||||
#[cfg(feature = "trace")]
|
#[cfg(feature = "trace")]
|
||||||
should_run_span.exit();
|
should_run_span.exit();
|
||||||
|
|
||||||
|
#[cfg(feature = "hotpatching")]
|
||||||
|
if should_update_hotpatch {
|
||||||
|
system.refresh_hotpatch();
|
||||||
|
}
|
||||||
|
|
||||||
// system has either been skipped or will run
|
// system has either been skipped or will run
|
||||||
self.completed_systems.insert(system_index);
|
self.completed_systems.insert(system_index);
|
||||||
|
|
||||||
@ -204,6 +217,12 @@ fn evaluate_and_fold_conditions(
|
|||||||
world: &mut World,
|
world: &mut World,
|
||||||
error_handler: ErrorHandler,
|
error_handler: ErrorHandler,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
|
#[cfg(feature = "hotpatching")]
|
||||||
|
let should_update_hotpatch = !world
|
||||||
|
.get_resource::<Events<HotPatched>>()
|
||||||
|
.map(Events::is_empty)
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
#[expect(
|
#[expect(
|
||||||
clippy::unnecessary_fold,
|
clippy::unnecessary_fold,
|
||||||
reason = "Short-circuiting here would prevent conditions from mutating their own state as needed."
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[cfg(feature = "hotpatching")]
|
||||||
|
if should_update_hotpatch {
|
||||||
|
condition.refresh_hotpatch();
|
||||||
|
}
|
||||||
__rust_begin_short_backtrace::readonly_run(&mut **condition, world)
|
__rust_begin_short_backtrace::readonly_run(&mut **condition, world)
|
||||||
})
|
})
|
||||||
.fold(true, |acc, res| acc && res)
|
.fold(true, |acc, res| acc && res)
|
||||||
|
@ -161,6 +161,12 @@ where
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "hotpatching")]
|
||||||
|
#[inline]
|
||||||
|
fn refresh_hotpatch(&mut self) {
|
||||||
|
self.system.refresh_hotpatch();
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn apply_deferred(&mut self, world: &mut crate::prelude::World) {
|
fn apply_deferred(&mut self, world: &mut crate::prelude::World) {
|
||||||
self.system.apply_deferred(world);
|
self.system.apply_deferred(world);
|
||||||
|
@ -182,6 +182,13 @@ where
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "hotpatching")]
|
||||||
|
#[inline]
|
||||||
|
fn refresh_hotpatch(&mut self) {
|
||||||
|
self.a.refresh_hotpatch();
|
||||||
|
self.b.refresh_hotpatch();
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn apply_deferred(&mut self, world: &mut World) {
|
fn apply_deferred(&mut self, world: &mut World) {
|
||||||
self.a.apply_deferred(world);
|
self.a.apply_deferred(world);
|
||||||
@ -392,6 +399,13 @@ where
|
|||||||
self.b.run_unsafe(value, world)
|
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) {
|
fn apply_deferred(&mut self, world: &mut World) {
|
||||||
self.a.apply_deferred(world);
|
self.a.apply_deferred(world);
|
||||||
self.b.apply_deferred(world);
|
self.b.apply_deferred(world);
|
||||||
|
@ -26,6 +26,8 @@ where
|
|||||||
F: ExclusiveSystemParamFunction<Marker>,
|
F: ExclusiveSystemParamFunction<Marker>,
|
||||||
{
|
{
|
||||||
func: F,
|
func: F,
|
||||||
|
#[cfg(feature = "hotpatching")]
|
||||||
|
current_ptr: subsecond::HotFnPtr,
|
||||||
param_state: Option<<F::Param as ExclusiveSystemParam>::State>,
|
param_state: Option<<F::Param as ExclusiveSystemParam>::State>,
|
||||||
system_meta: SystemMeta,
|
system_meta: SystemMeta,
|
||||||
// NOTE: PhantomData<fn()-> T> gives this safe Send/Sync impls
|
// NOTE: PhantomData<fn()-> T> gives this safe Send/Sync impls
|
||||||
@ -58,6 +60,11 @@ where
|
|||||||
fn into_system(func: Self) -> Self::System {
|
fn into_system(func: Self) -> Self::System {
|
||||||
ExclusiveFunctionSystem {
|
ExclusiveFunctionSystem {
|
||||||
func,
|
func,
|
||||||
|
#[cfg(feature = "hotpatching")]
|
||||||
|
current_ptr: subsecond::HotFn::current(
|
||||||
|
<F as ExclusiveSystemParamFunction<Marker>>::run,
|
||||||
|
)
|
||||||
|
.ptr_address(),
|
||||||
param_state: None,
|
param_state: None,
|
||||||
system_meta: SystemMeta::new::<F>(),
|
system_meta: SystemMeta::new::<F>(),
|
||||||
marker: PhantomData,
|
marker: PhantomData,
|
||||||
@ -125,6 +132,20 @@ where
|
|||||||
self.param_state.as_mut().expect(PARAM_MESSAGE),
|
self.param_state.as_mut().expect(PARAM_MESSAGE),
|
||||||
&self.system_meta,
|
&self.system_meta,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
#[cfg(feature = "hotpatching")]
|
||||||
|
let out = {
|
||||||
|
let mut hot_fn =
|
||||||
|
subsecond::HotFn::current(<F as ExclusiveSystemParamFunction<Marker>>::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);
|
let out = self.func.run(world, input, params);
|
||||||
|
|
||||||
world.flush();
|
world.flush();
|
||||||
@ -134,6 +155,17 @@ where
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "hotpatching")]
|
||||||
|
#[inline]
|
||||||
|
fn refresh_hotpatch(&mut self) {
|
||||||
|
let new = subsecond::HotFn::current(<F as ExclusiveSystemParamFunction<Marker>>::run)
|
||||||
|
.ptr_address();
|
||||||
|
if new != self.current_ptr {
|
||||||
|
log::debug!("system {} hotpatched", self.name());
|
||||||
|
}
|
||||||
|
self.current_ptr = new;
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn apply_deferred(&mut self, _world: &mut World) {
|
fn apply_deferred(&mut self, _world: &mut World) {
|
||||||
// "pure" exclusive systems do not have any buffers to apply.
|
// "pure" exclusive systems do not have any buffers to apply.
|
||||||
|
@ -306,6 +306,9 @@ impl<Param: SystemParam> SystemState<Param> {
|
|||||||
) -> FunctionSystem<Marker, F> {
|
) -> FunctionSystem<Marker, F> {
|
||||||
FunctionSystem {
|
FunctionSystem {
|
||||||
func,
|
func,
|
||||||
|
#[cfg(feature = "hotpatching")]
|
||||||
|
current_ptr: subsecond::HotFn::current(<F as SystemParamFunction<Marker>>::run)
|
||||||
|
.ptr_address(),
|
||||||
state: Some(FunctionSystemState {
|
state: Some(FunctionSystemState {
|
||||||
param: self.param_state,
|
param: self.param_state,
|
||||||
world_id: self.world_id,
|
world_id: self.world_id,
|
||||||
@ -519,6 +522,8 @@ where
|
|||||||
F: SystemParamFunction<Marker>,
|
F: SystemParamFunction<Marker>,
|
||||||
{
|
{
|
||||||
func: F,
|
func: F,
|
||||||
|
#[cfg(feature = "hotpatching")]
|
||||||
|
current_ptr: subsecond::HotFnPtr,
|
||||||
state: Option<FunctionSystemState<F::Param>>,
|
state: Option<FunctionSystemState<F::Param>>,
|
||||||
system_meta: SystemMeta,
|
system_meta: SystemMeta,
|
||||||
// NOTE: PhantomData<fn()-> T> gives this safe Send/Sync impls
|
// NOTE: PhantomData<fn()-> T> gives this safe Send/Sync impls
|
||||||
@ -558,6 +563,9 @@ where
|
|||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
func: self.func.clone(),
|
func: self.func.clone(),
|
||||||
|
#[cfg(feature = "hotpatching")]
|
||||||
|
current_ptr: subsecond::HotFn::current(<F as SystemParamFunction<Marker>>::run)
|
||||||
|
.ptr_address(),
|
||||||
state: None,
|
state: None,
|
||||||
system_meta: SystemMeta::new::<F>(),
|
system_meta: SystemMeta::new::<F>(),
|
||||||
marker: PhantomData,
|
marker: PhantomData,
|
||||||
@ -578,6 +586,9 @@ where
|
|||||||
fn into_system(func: Self) -> Self::System {
|
fn into_system(func: Self) -> Self::System {
|
||||||
FunctionSystem {
|
FunctionSystem {
|
||||||
func,
|
func,
|
||||||
|
#[cfg(feature = "hotpatching")]
|
||||||
|
current_ptr: subsecond::HotFn::current(<F as SystemParamFunction<Marker>>::run)
|
||||||
|
.ptr_address(),
|
||||||
state: None,
|
state: None,
|
||||||
system_meta: SystemMeta::new::<F>(),
|
system_meta: SystemMeta::new::<F>(),
|
||||||
marker: PhantomData,
|
marker: PhantomData,
|
||||||
@ -653,11 +664,35 @@ where
|
|||||||
// will ensure that there are no data access conflicts.
|
// will ensure that there are no data access conflicts.
|
||||||
let params =
|
let params =
|
||||||
unsafe { F::Param::get_param(&mut state.param, &self.system_meta, world, change_tick) };
|
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(<F as SystemParamFunction<Marker>>::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);
|
let out = self.func.run(input, params);
|
||||||
|
|
||||||
self.system_meta.last_run = change_tick;
|
self.system_meta.last_run = change_tick;
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "hotpatching")]
|
||||||
|
#[inline]
|
||||||
|
fn refresh_hotpatch(&mut self) {
|
||||||
|
let new = subsecond::HotFn::current(<F as SystemParamFunction<Marker>>::run).ptr_address();
|
||||||
|
if new != self.current_ptr {
|
||||||
|
log::debug!("system {} hotpatched", self.name());
|
||||||
|
}
|
||||||
|
self.current_ptr = new;
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn apply_deferred(&mut self, world: &mut World) {
|
fn apply_deferred(&mut self, world: &mut World) {
|
||||||
let param_state = &mut self.state.as_mut().expect(Self::ERROR_UNINITIALIZED).param;
|
let param_state = &mut self.state.as_mut().expect(Self::ERROR_UNINITIALIZED).param;
|
||||||
|
@ -151,6 +151,12 @@ where
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "hotpatching")]
|
||||||
|
#[inline]
|
||||||
|
fn refresh_hotpatch(&mut self) {
|
||||||
|
self.observer.refresh_hotpatch();
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn apply_deferred(&mut self, world: &mut World) {
|
fn apply_deferred(&mut self, world: &mut World) {
|
||||||
self.observer.apply_deferred(world);
|
self.observer.apply_deferred(world);
|
||||||
|
@ -68,6 +68,12 @@ impl<S: System<In = ()>> System for InfallibleSystemWrapper<S> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "hotpatching")]
|
||||||
|
#[inline]
|
||||||
|
fn refresh_hotpatch(&mut self) {
|
||||||
|
self.0.refresh_hotpatch();
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn apply_deferred(&mut self, world: &mut World) {
|
fn apply_deferred(&mut self, world: &mut World) {
|
||||||
self.0.apply_deferred(world);
|
self.0.apply_deferred(world);
|
||||||
@ -186,6 +192,12 @@ where
|
|||||||
self.system.run_unsafe(&mut self.value, world)
|
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) {
|
fn apply_deferred(&mut self, world: &mut World) {
|
||||||
self.system.apply_deferred(world);
|
self.system.apply_deferred(world);
|
||||||
}
|
}
|
||||||
@ -293,6 +305,12 @@ where
|
|||||||
self.system.run_unsafe(value, world)
|
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) {
|
fn apply_deferred(&mut self, world: &mut World) {
|
||||||
self.system.apply_deferred(world);
|
self.system.apply_deferred(world);
|
||||||
}
|
}
|
||||||
|
@ -76,6 +76,10 @@ pub trait System: Send + Sync + 'static {
|
|||||||
unsafe fn run_unsafe(&mut self, input: SystemIn<'_, Self>, world: UnsafeWorldCell)
|
unsafe fn run_unsafe(&mut self, input: SystemIn<'_, Self>, world: UnsafeWorldCell)
|
||||||
-> Self::Out;
|
-> 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.
|
/// Runs the system with the given input in the world.
|
||||||
///
|
///
|
||||||
/// For [read-only](ReadOnlySystem) systems, see [`run_readonly`], which can be called using `&World`.
|
/// For [read-only](ReadOnlySystem) systems, see [`run_readonly`], which can be called using `&World`.
|
||||||
|
@ -344,6 +344,8 @@ web = [
|
|||||||
"bevy_tasks/web",
|
"bevy_tasks/web",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
hotpatching = ["bevy_app/hotpatching", "bevy_ecs/hotpatching"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# bevy (no_std)
|
# bevy (no_std)
|
||||||
bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [
|
bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [
|
||||||
|
@ -66,6 +66,8 @@ plugin_group! {
|
|||||||
bevy_dev_tools:::DevToolsPlugin,
|
bevy_dev_tools:::DevToolsPlugin,
|
||||||
#[cfg(feature = "bevy_ci_testing")]
|
#[cfg(feature = "bevy_ci_testing")]
|
||||||
bevy_dev_tools::ci_testing:::CiTestingPlugin,
|
bevy_dev_tools::ci_testing:::CiTestingPlugin,
|
||||||
|
#[cfg(feature = "hotpatching")]
|
||||||
|
bevy_app::hotpatch:::HotPatchPlugin,
|
||||||
#[plugin_group]
|
#[plugin_group]
|
||||||
#[cfg(feature = "bevy_picking")]
|
#[cfg(feature = "bevy_picking")]
|
||||||
bevy_picking:::DefaultPickingPlugins,
|
bevy_picking:::DefaultPickingPlugins,
|
||||||
|
@ -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|
|
|ghost_nodes|Experimental support for nodes that are ignored for UI layouting|
|
||||||
|gif|GIF image format support|
|
|gif|GIF image format support|
|
||||||
|glam_assert|Enable assertions to check the validity of parameters passed to glam|
|
|glam_assert|Enable assertions to check the validity of parameters passed to glam|
|
||||||
|
|hotpatching|Enable hotpatching of Bevy systems|
|
||||||
|ico|ICO image format support|
|
|ico|ICO image format support|
|
||||||
|jpeg|JPEG image format support|
|
|jpeg|JPEG image format support|
|
||||||
|libm|Uses the `libm` maths library instead of the one provided in `std` and `core`.|
|
|libm|Uses the `libm` maths library instead of the one provided in `std` and `core`.|
|
||||||
|
@ -319,6 +319,7 @@ Example | Description
|
|||||||
[Fixed Timestep](../examples/ecs/fixed_timestep.rs) | Shows how to create systems that run every fixed timestep, rather than every tick
|
[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
|
[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
|
[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
|
[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
|
[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.
|
[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.
|
||||||
|
94
examples/ecs/hotpatching_systems.rs
Normal file
94
examples/ecs/hotpatching_systems.rs
Normal file
@ -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<Pointer<Click>>,
|
||||||
|
mut color: Single<&mut TextColor>,
|
||||||
|
task_sender: Res<TaskSender>,
|
||||||
|
) {
|
||||||
|
// 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());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
21
release-content/release-notes/hot_patching.md
Normal file
21
release-content/release-notes/hot_patching.md
Normal file
@ -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)
|
Loading…
Reference in New Issue
Block a user