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.
|
||||
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 }
|
||||
@ -4411,3 +4414,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
|
||||
|
@ -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(all(unix, not(target_os = "horizon")), windows))'.dependencies]
|
||||
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"))]
|
||||
mod terminal_ctrl_c_handler;
|
||||
|
||||
#[cfg(feature = "hotpatching")]
|
||||
pub mod hotpatch;
|
||||
|
||||
pub use app::*;
|
||||
pub use main_schedule::*;
|
||||
pub use panic_handler::*;
|
||||
|
@ -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]
|
||||
|
@ -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::{
|
||||
|
@ -371,6 +371,11 @@ fn observer_system_runner<E: Event, B: Bundle, S: ObserverSystem<E, B>>(
|
||||
// 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) {
|
||||
|
@ -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.
|
||||
|
@ -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::<Events<HotPatched>>()
|
||||
.map(Events::is_empty)
|
||||
.unwrap_or(true);
|
||||
|
||||
// can't borrow since loop mutably borrows `self`
|
||||
let mut ready_systems = core::mem::take(&mut self.ready_systems_copy);
|
||||
|
||||
@ -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)
|
||||
|
@ -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::<Events<HotPatched>>()
|
||||
.map(Events::is_empty)
|
||||
.unwrap_or(true);
|
||||
|
||||
for system_index in 0..schedule.systems.len() {
|
||||
#[cfg(feature = "trace")]
|
||||
let name = schedule.systems[system_index].name();
|
||||
@ -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::<Events<HotPatched>>()
|
||||
.map(Events::is_empty)
|
||||
.unwrap_or(true);
|
||||
|
||||
#[expect(
|
||||
clippy::unnecessary_fold,
|
||||
reason = "Short-circuiting here would prevent conditions from mutating their own state as needed."
|
||||
@ -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)
|
||||
|
@ -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::<Events<HotPatched>>()
|
||||
.map(Events::is_empty)
|
||||
.unwrap_or(true);
|
||||
|
||||
for system_index in 0..schedule.systems.len() {
|
||||
#[cfg(feature = "trace")]
|
||||
let name = schedule.systems[system_index].name();
|
||||
@ -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::<Events<HotPatched>>()
|
||||
.map(Events::is_empty)
|
||||
.unwrap_or(true);
|
||||
|
||||
#[expect(
|
||||
clippy::unnecessary_fold,
|
||||
reason = "Short-circuiting here would prevent conditions from mutating their own state as needed."
|
||||
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -26,6 +26,8 @@ where
|
||||
F: ExclusiveSystemParamFunction<Marker>,
|
||||
{
|
||||
func: F,
|
||||
#[cfg(feature = "hotpatching")]
|
||||
current_ptr: subsecond::HotFnPtr,
|
||||
param_state: Option<<F::Param as ExclusiveSystemParam>::State>,
|
||||
system_meta: SystemMeta,
|
||||
// NOTE: PhantomData<fn()-> 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(
|
||||
<F as ExclusiveSystemParamFunction<Marker>>::run,
|
||||
)
|
||||
.ptr_address(),
|
||||
param_state: None,
|
||||
system_meta: SystemMeta::new::<F>(),
|
||||
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(<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);
|
||||
|
||||
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]
|
||||
fn apply_deferred(&mut self, _world: &mut World) {
|
||||
// "pure" exclusive systems do not have any buffers to apply.
|
||||
|
@ -306,6 +306,9 @@ impl<Param: SystemParam> SystemState<Param> {
|
||||
) -> FunctionSystem<Marker, F> {
|
||||
FunctionSystem {
|
||||
func,
|
||||
#[cfg(feature = "hotpatching")]
|
||||
current_ptr: subsecond::HotFn::current(<F as SystemParamFunction<Marker>>::run)
|
||||
.ptr_address(),
|
||||
state: Some(FunctionSystemState {
|
||||
param: self.param_state,
|
||||
world_id: self.world_id,
|
||||
@ -519,6 +522,8 @@ where
|
||||
F: SystemParamFunction<Marker>,
|
||||
{
|
||||
func: F,
|
||||
#[cfg(feature = "hotpatching")]
|
||||
current_ptr: subsecond::HotFnPtr,
|
||||
state: Option<FunctionSystemState<F::Param>>,
|
||||
system_meta: SystemMeta,
|
||||
// NOTE: PhantomData<fn()-> 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(<F as SystemParamFunction<Marker>>::run)
|
||||
.ptr_address(),
|
||||
state: None,
|
||||
system_meta: SystemMeta::new::<F>(),
|
||||
marker: PhantomData,
|
||||
@ -578,6 +586,9 @@ where
|
||||
fn into_system(func: Self) -> Self::System {
|
||||
FunctionSystem {
|
||||
func,
|
||||
#[cfg(feature = "hotpatching")]
|
||||
current_ptr: subsecond::HotFn::current(<F as SystemParamFunction<Marker>>::run)
|
||||
.ptr_address(),
|
||||
state: None,
|
||||
system_meta: SystemMeta::new::<F>(),
|
||||
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(<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);
|
||||
|
||||
self.system_meta.last_run = change_tick;
|
||||
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]
|
||||
fn apply_deferred(&mut self, world: &mut World) {
|
||||
let param_state = &mut self.state.as_mut().expect(Self::ERROR_UNINITIALIZED).param;
|
||||
|
@ -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);
|
||||
|
@ -68,6 +68,12 @@ impl<S: System<In = ()>> System for InfallibleSystemWrapper<S> {
|
||||
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);
|
||||
}
|
||||
|
@ -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`.
|
||||
|
@ -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 = [
|
||||
|
@ -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,
|
||||
|
@ -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`.|
|
||||
|
@ -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
|
||||
[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.
|
||||
|
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