Skip to content

Commit 98938a8

Browse files
committed
Internal Asset Hot Reloading (#3966)
Adds "hot reloading" of internal assets, which is normally not possible because they are loaded using `include_str` / direct Asset collection access. This is accomplished via the following: * Add a new `debug_asset_server` feature flag * When that feature flag is enabled, create a second App with a second AssetServer that points to a configured location (by default the `crates` folder). Plugins that want to add hot reloading support for their assets can call the new `app.add_debug_asset::<T>()` and `app.init_debug_asset_loader::<T>()` functions. * Load "internal" assets using the new `load_internal_asset` macro. By default this is identical to the current "include_str + register in asset collection" approach. But if the `debug_asset_server` feature flag is enabled, it will also load the asset dynamically in the debug asset server using the file path. It will then set up a correlation between the "debug asset" and the "actual asset" by listening for asset change events. This is an alternative to #3673. The goal was to keep the boilerplate and features flags to a minimum for bevy plugin authors, and allow them to home their shaders near relevant code. This is a draft because I haven't done _any_ quality control on this yet. I'll probably rename things and remove a bunch of unwraps. I just got it working and wanted to use it to start a conversation. Fixes #3660
1 parent e9f52b9 commit 98938a8

File tree

17 files changed

+271
-40
lines changed

17 files changed

+271
-40
lines changed

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ subpixel_glyph_atlas = ["bevy_internal/subpixel_glyph_atlas"]
9090
# Enable systems that allow for automated testing on CI
9191
bevy_ci_testing = ["bevy_internal/bevy_ci_testing"]
9292

93+
# Enable the "debug asset server" for hot reloading internal assets
94+
debug_asset_server = ["bevy_internal/debug_asset_server"]
95+
9396
[dependencies]
9497
bevy_dylib = { path = "crates/bevy_dylib", version = "0.6.0", default-features = false, optional = true }
9598
bevy_internal = { path = "crates/bevy_internal", version = "0.6.0", default-features = false }

crates/bevy_asset/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ keywords = ["bevy"]
1111
[features]
1212
default = []
1313
filesystem_watcher = ["notify"]
14+
debug_asset_server = ["filesystem_watcher"]
1415

1516
[dependencies]
1617
# bevy

crates/bevy_asset/src/asset_server.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ impl AssetServer {
9292
}
9393
}
9494

95+
pub fn asset_io(&self) -> &dyn AssetIo {
96+
&*self.server.asset_io
97+
}
98+
9599
pub(crate) fn register_asset_type<T: Asset>(&self) -> Assets<T> {
96100
if self
97101
.server

crates/bevy_asset/src/assets.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,9 +259,15 @@ impl<T: Asset> Assets<T> {
259259
/// [App] extension methods for adding new asset types
260260
pub trait AddAsset {
261261
fn add_asset<T>(&mut self) -> &mut Self
262+
where
263+
T: Asset;
264+
fn add_debug_asset<T: Clone>(&mut self) -> &mut Self
262265
where
263266
T: Asset;
264267
fn init_asset_loader<T>(&mut self) -> &mut Self
268+
where
269+
T: AssetLoader + FromWorld;
270+
fn init_debug_asset_loader<T>(&mut self) -> &mut Self
265271
where
266272
T: AssetLoader + FromWorld;
267273
fn add_asset_loader<T>(&mut self, loader: T) -> &mut Self
@@ -292,6 +298,23 @@ impl AddAsset for App {
292298
.add_event::<AssetEvent<T>>()
293299
}
294300

301+
fn add_debug_asset<T: Clone>(&mut self) -> &mut Self
302+
where
303+
T: Asset,
304+
{
305+
#[cfg(feature = "debug_asset_server")]
306+
{
307+
self.add_system(crate::debug_asset_server::sync_debug_assets::<T>);
308+
let mut app = self
309+
.world
310+
.get_non_send_resource_mut::<crate::debug_asset_server::DebugAssetApp>()
311+
.unwrap();
312+
app.add_asset::<T>()
313+
.init_resource::<crate::debug_asset_server::HandleMap<T>>();
314+
}
315+
self
316+
}
317+
295318
fn init_asset_loader<T>(&mut self) -> &mut Self
296319
where
297320
T: AssetLoader + FromWorld,
@@ -300,6 +323,21 @@ impl AddAsset for App {
300323
self.add_asset_loader(result)
301324
}
302325

326+
fn init_debug_asset_loader<T>(&mut self) -> &mut Self
327+
where
328+
T: AssetLoader + FromWorld,
329+
{
330+
#[cfg(feature = "debug_asset_server")]
331+
{
332+
let mut app = self
333+
.world
334+
.get_non_send_resource_mut::<crate::debug_asset_server::DebugAssetApp>()
335+
.unwrap();
336+
app.init_asset_loader::<T>();
337+
}
338+
self
339+
}
340+
303341
fn add_asset_loader<T>(&mut self, loader: T) -> &mut Self
304342
where
305343
T: AssetLoader,
@@ -312,6 +350,43 @@ impl AddAsset for App {
312350
}
313351
}
314352

353+
#[cfg(feature = "debug_asset_server")]
354+
#[macro_export]
355+
macro_rules! load_internal_asset {
356+
($app: ident, $handle: ident, $path_str: expr, $loader: expr) => {{
357+
{
358+
let mut debug_app = $app
359+
.world
360+
.get_non_send_resource_mut::<bevy_asset::debug_asset_server::DebugAssetApp>()
361+
.unwrap();
362+
bevy_asset::debug_asset_server::register_handle_with_loader(
363+
$loader,
364+
&mut debug_app,
365+
$handle,
366+
file!(),
367+
$path_str,
368+
);
369+
}
370+
let mut assets = $app
371+
.world
372+
.get_resource_mut::<bevy_asset::Assets<_>>()
373+
.unwrap();
374+
assets.set_untracked($handle, ($loader)(include_str!($path_str)));
375+
}};
376+
}
377+
378+
#[cfg(not(feature = "debug_asset_server"))]
379+
#[macro_export]
380+
macro_rules! load_internal_asset {
381+
($app: ident, $handle: ident, $path_str: expr, $loader: expr) => {{
382+
let mut assets = $app
383+
.world
384+
.get_resource_mut::<bevy_asset::Assets<_>>()
385+
.unwrap();
386+
assets.set_untracked($handle, ($loader)(include_str!($path_str)));
387+
}};
388+
}
389+
315390
#[cfg(test)]
316391
mod tests {
317392
use bevy_app::App;
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
use bevy_app::{App, Events, Plugin};
2+
use bevy_ecs::{
3+
schedule::SystemLabel,
4+
system::{NonSendMut, Res, ResMut, SystemState},
5+
};
6+
use bevy_tasks::{IoTaskPool, TaskPoolBuilder};
7+
use bevy_utils::HashMap;
8+
use std::{
9+
ops::{Deref, DerefMut},
10+
path::Path,
11+
};
12+
13+
use crate::{
14+
Asset, AssetEvent, AssetPlugin, AssetServer, AssetServerSettings, Assets, FileAssetIo, Handle,
15+
HandleUntyped,
16+
};
17+
18+
/// A "debug asset app", whose sole responsibility is hot reloading assets that are
19+
/// "internal" / compiled-in to Bevy Plugins.
20+
pub struct DebugAssetApp(App);
21+
22+
impl Deref for DebugAssetApp {
23+
type Target = App;
24+
25+
fn deref(&self) -> &Self::Target {
26+
&self.0
27+
}
28+
}
29+
30+
impl DerefMut for DebugAssetApp {
31+
fn deref_mut(&mut self) -> &mut Self::Target {
32+
&mut self.0
33+
}
34+
}
35+
36+
#[derive(SystemLabel, Debug, Clone, PartialEq, Eq, Hash)]
37+
pub struct DebugAssetAppRun;
38+
39+
/// Facilitates the creation of a "debug asset app", whose sole responsibility is hot reloading
40+
/// assets that are "internal" / compiled-in to Bevy Plugins.
41+
/// Pair with [`load_internal_asset`](crate::load_internal_asset) to load "hot reloadable" assets
42+
/// The `debug_asset_server` feature flag must also be enabled for hot reloading to work.
43+
/// Currently only hot reloads assets stored in the `crates` folder.
44+
#[derive(Default)]
45+
pub struct DebugAssetServerPlugin;
46+
pub struct HandleMap<T: Asset> {
47+
pub handles: HashMap<Handle<T>, Handle<T>>,
48+
}
49+
50+
impl<T: Asset> Default for HandleMap<T> {
51+
fn default() -> Self {
52+
Self {
53+
handles: Default::default(),
54+
}
55+
}
56+
}
57+
58+
impl Plugin for DebugAssetServerPlugin {
59+
fn build(&self, app: &mut bevy_app::App) {
60+
let mut debug_asset_app = App::new();
61+
debug_asset_app
62+
.insert_resource(IoTaskPool(
63+
TaskPoolBuilder::default()
64+
.num_threads(2)
65+
.thread_name("Debug Asset Server IO Task Pool".to_string())
66+
.build(),
67+
))
68+
.insert_resource(AssetServerSettings {
69+
asset_folder: "crates".to_string(),
70+
watch_for_changes: true,
71+
})
72+
.add_plugin(AssetPlugin);
73+
app.insert_non_send_resource(DebugAssetApp(debug_asset_app));
74+
app.add_system(run_debug_asset_app);
75+
}
76+
}
77+
78+
fn run_debug_asset_app(mut debug_asset_app: NonSendMut<DebugAssetApp>) {
79+
debug_asset_app.0.update();
80+
}
81+
82+
pub(crate) fn sync_debug_assets<T: Asset + Clone>(
83+
mut debug_asset_app: NonSendMut<DebugAssetApp>,
84+
mut assets: ResMut<Assets<T>>,
85+
) {
86+
let world = &mut debug_asset_app.0.world;
87+
let mut state = SystemState::<(
88+
Res<Events<AssetEvent<T>>>,
89+
Res<HandleMap<T>>,
90+
Res<Assets<T>>,
91+
)>::new(world);
92+
let (changed_shaders, handle_map, debug_assets) = state.get_mut(world);
93+
for changed in changed_shaders.iter_current_update_events() {
94+
let debug_handle = match changed {
95+
AssetEvent::Created { handle } => handle,
96+
AssetEvent::Modified { handle } => handle,
97+
AssetEvent::Removed { .. } => continue,
98+
};
99+
if let Some(handle) = handle_map.handles.get(debug_handle) {
100+
if let Some(debug_asset) = debug_assets.get(debug_handle) {
101+
assets.set_untracked(handle, debug_asset.clone());
102+
}
103+
}
104+
}
105+
}
106+
107+
/// Uses the return type of the given loader to register the given handle with the appropriate type
108+
/// and load the asset with the given `path` and parent `file_path`.
109+
/// If this feels a bit odd ... thats because it is. This was built to improve the UX of the
110+
/// `load_internal_asset` macro.
111+
pub fn register_handle_with_loader<A: Asset>(
112+
_loader: fn(&'static str) -> A,
113+
app: &mut DebugAssetApp,
114+
handle: HandleUntyped,
115+
file_path: &str,
116+
path: &'static str,
117+
) {
118+
let mut state = SystemState::<(ResMut<HandleMap<A>>, Res<AssetServer>)>::new(&mut app.world);
119+
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
120+
let manifest_dir_path = Path::new(&manifest_dir);
121+
let (mut handle_map, asset_server) = state.get_mut(&mut app.world);
122+
let asset_io = asset_server
123+
.asset_io()
124+
.downcast_ref::<FileAssetIo>()
125+
.expect("The debug AssetServer only works with FileAssetIo-backed AssetServers");
126+
let absolute_file_path = manifest_dir_path.join(
127+
Path::new(file_path)
128+
.parent()
129+
.expect("file path must have a parent"),
130+
);
131+
let asset_folder_relative_path = absolute_file_path
132+
.strip_prefix(asset_io.root_path())
133+
.expect("The AssetIo root path should be a prefix of the absolute file path");
134+
handle_map.handles.insert(
135+
asset_server.load(asset_folder_relative_path.join(path)),
136+
handle.clone_weak().typed::<A>(),
137+
);
138+
}

crates/bevy_asset/src/io/file_asset_io.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ impl FileAssetIo {
6262
.unwrap()
6363
}
6464
}
65+
66+
pub fn root_path(&self) -> &PathBuf {
67+
&self.root_path
68+
}
6569
}
6670

6771
impl AssetIo for FileAssetIo {

crates/bevy_asset/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
mod asset_server;
22
mod assets;
3+
#[cfg(feature = "debug_asset_server")]
4+
pub mod debug_asset_server;
35
pub mod diagnostic;
46
#[cfg(all(
57
feature = "filesystem_watcher",

crates/bevy_internal/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ trace = [ "bevy_app/trace", "bevy_ecs/trace", "bevy_render/trace" ]
1414
trace_chrome = [ "bevy_log/tracing-chrome" ]
1515
trace_tracy = [ "bevy_log/tracing-tracy" ]
1616
wgpu_trace = ["bevy_render/wgpu_trace"]
17+
debug_asset_server = ["bevy_asset/debug_asset_server"]
1718

1819
# Image format support for texture loading (PNG and HDR are enabled by default)
1920
hdr = ["bevy_render/hdr"]

crates/bevy_internal/src/default_plugins.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ impl PluginGroup for DefaultPlugins {
3131
group.add(bevy_input::InputPlugin::default());
3232
group.add(bevy_window::WindowPlugin::default());
3333
group.add(bevy_asset::AssetPlugin::default());
34+
#[cfg(feature = "debug_asset_server")]
35+
group.add(bevy_asset::debug_asset_server::DebugAssetServerPlugin::default());
3436
group.add(bevy_scene::ScenePlugin::default());
3537

3638
#[cfg(feature = "bevy_winit")]

crates/bevy_pbr/src/lib.rs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ pub mod draw_3d_graph {
3333
}
3434

3535
use bevy_app::prelude::*;
36-
use bevy_asset::{Assets, Handle, HandleUntyped};
36+
use bevy_asset::{load_internal_asset, Assets, Handle, HandleUntyped};
3737
use bevy_ecs::prelude::*;
3838
use bevy_reflect::TypeUuid;
3939
use bevy_render::{
@@ -57,14 +57,12 @@ pub struct PbrPlugin;
5757

5858
impl Plugin for PbrPlugin {
5959
fn build(&self, app: &mut App) {
60-
let mut shaders = app.world.get_resource_mut::<Assets<Shader>>().unwrap();
61-
shaders.set_untracked(
62-
PBR_SHADER_HANDLE,
63-
Shader::from_wgsl(include_str!("render/pbr.wgsl")),
64-
);
65-
shaders.set_untracked(
60+
load_internal_asset!(app, PBR_SHADER_HANDLE, "render/pbr.wgsl", Shader::from_wgsl);
61+
load_internal_asset!(
62+
app,
6663
SHADOW_SHADER_HANDLE,
67-
Shader::from_wgsl(include_str!("render/depth.wgsl")),
64+
"render/depth.wgsl",
65+
Shader::from_wgsl
6866
);
6967

7068
app.register_type::<CubemapVisibleEntities>()

crates/bevy_pbr/src/render/mesh.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::{
33
ViewClusterBindings, ViewLightsUniformOffset, ViewShadowBindings,
44
};
55
use bevy_app::Plugin;
6-
use bevy_asset::{Assets, Handle, HandleUntyped};
6+
use bevy_asset::{load_internal_asset, Handle, HandleUntyped};
77
use bevy_ecs::{
88
prelude::*,
99
system::{lifetimeless::*, SystemParamItem},
@@ -35,18 +35,18 @@ pub const MESH_SHADER_HANDLE: HandleUntyped =
3535

3636
impl Plugin for MeshRenderPlugin {
3737
fn build(&self, app: &mut bevy_app::App) {
38-
let mut shaders = app.world.get_resource_mut::<Assets<Shader>>().unwrap();
39-
shaders.set_untracked(
40-
MESH_SHADER_HANDLE,
41-
Shader::from_wgsl(include_str!("mesh.wgsl")),
42-
);
43-
shaders.set_untracked(
38+
load_internal_asset!(app, MESH_SHADER_HANDLE, "mesh.wgsl", Shader::from_wgsl);
39+
load_internal_asset!(
40+
app,
4441
MESH_STRUCT_HANDLE,
45-
Shader::from_wgsl(include_str!("mesh_struct.wgsl")),
42+
"mesh_struct.wgsl",
43+
Shader::from_wgsl
4644
);
47-
shaders.set_untracked(
45+
load_internal_asset!(
46+
app,
4847
MESH_VIEW_BIND_GROUP_HANDLE,
49-
Shader::from_wgsl(include_str!("mesh_view_bind_group.wgsl")),
48+
"mesh_view_bind_group.wgsl",
49+
Shader::from_wgsl
5050
);
5151

5252
app.add_plugin(UniformComponentPlugin::<MeshUniform>::default());

crates/bevy_pbr/src/wireframe.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::MeshPipeline;
22
use crate::{DrawMesh, MeshPipelineKey, MeshUniform, SetMeshBindGroup, SetMeshViewBindGroup};
33
use bevy_app::Plugin;
4-
use bevy_asset::{Assets, Handle, HandleUntyped};
4+
use bevy_asset::{load_internal_asset, Handle, HandleUntyped};
55
use bevy_core_pipeline::Opaque3d;
66
use bevy_ecs::{prelude::*, reflect::ReflectComponent};
77
use bevy_reflect::{Reflect, TypeUuid};
@@ -23,10 +23,11 @@ pub struct WireframePlugin;
2323

2424
impl Plugin for WireframePlugin {
2525
fn build(&self, app: &mut bevy_app::App) {
26-
let mut shaders = app.world.get_resource_mut::<Assets<Shader>>().unwrap();
27-
shaders.set_untracked(
26+
load_internal_asset!(
27+
app,
2828
WIREFRAME_SHADER_HANDLE,
29-
Shader::from_wgsl(include_str!("render/wireframe.wgsl")),
29+
"render/wireframe.wgsl",
30+
Shader::from_wgsl
3031
);
3132

3233
app.init_resource::<WireframeConfig>();

0 commit comments

Comments
 (0)