Skip to content

Hot patching systems with subsecond #19309

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 27 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


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

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

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

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

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

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

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

[lints]
workspace = true
Expand Down
42 changes: 42 additions & 0 deletions crates/bevy_dev_tools/src/hotpatch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//! Utilities for hotpatching code.
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();
}
},
);
}
}
5 changes: 4 additions & 1 deletion crates/bevy_dev_tools/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![forbid(unsafe_code)]
#![cfg_attr(not(feature = "hotpatching"), forbid(unsafe_code))]
#![doc(
html_logo_url = "https://bevyengine.org/assets/icon.png",
html_favicon_url = "https://bevyengine.org/assets/icon.png"
Expand All @@ -13,6 +13,9 @@ use bevy_app::prelude::*;
#[cfg(feature = "bevy_ci_testing")]
pub mod ci_testing;

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

pub mod fps_overlay;

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

hotpatching = ["dep:subsecond"]

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

Choose a reason for hiding this comment

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

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

Copy link
Member

Choose a reason for hiding this comment

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

Based on the PR description I think this is meant to be temporary


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

pub use bevy_ptr as ptr;

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

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

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

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

(*system).update_archetype_component_access(world);
match (*system).validate_param_unsafe(world) {
Ok(()) => {
Expand Down
4 changes: 4 additions & 0 deletions crates/bevy_ecs/src/schedule/executor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,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.
Expand Down
15 changes: 15 additions & 0 deletions crates/bevy_ecs/src/schedule/executor/multi_threaded.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ use crate::{
system::ScheduleSystem,
world::{unsafe_world_cell::UnsafeWorldCell, World},
};
#[cfg(feature = "hotpatching")]
use crate::{event::Events, HotPatched};

use super::__rust_begin_short_backtrace;

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

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

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

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

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

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

use super::__rust_begin_short_backtrace;

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

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

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

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

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

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

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

use super::__rust_begin_short_backtrace;

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

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

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

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

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

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

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