Skip to content

Commit 17f045e

Browse files
JMS55mockersf
andauthored
Delay asset hot reloading (#8503)
# Objective - Fix #5631 ## Solution - Wait 50ms (configurable) after the last modification event before reloading an asset. --- ## Changelog - `AssetPlugin::watch_for_changes` is now a `ChangeWatcher` instead of a `bool` - Fixed #5631 ## Migration Guide - Replace `AssetPlugin::watch_for_changes: true` with e.g. `ChangeWatcher::with_delay(Duration::from_millis(200))` --------- Co-authored-by: François <mockersf@gmail.com>
1 parent 0736195 commit 17f045e

File tree

13 files changed

+89
-41
lines changed

13 files changed

+89
-41
lines changed

crates/bevy_asset/src/asset_server.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,11 @@ pub struct AssetServerInternal {
8282
/// ```
8383
/// # use bevy_asset::*;
8484
/// # use bevy_app::*;
85+
/// # use bevy_utils::Duration;
8586
/// # let mut app = App::new();
8687
/// // The asset plugin can be configured to watch for asset changes.
8788
/// app.add_plugin(AssetPlugin {
88-
/// watch_for_changes: true,
89+
/// watch_for_changes: ChangeWatcher::with_delay(Duration::from_millis(200)),
8990
/// ..Default::default()
9091
/// });
9192
/// ```
@@ -702,7 +703,7 @@ mod test {
702703
fn setup(asset_path: impl AsRef<Path>) -> AssetServer {
703704
use crate::FileAssetIo;
704705
IoTaskPool::init(Default::default);
705-
AssetServer::new(FileAssetIo::new(asset_path, false))
706+
AssetServer::new(FileAssetIo::new(asset_path, &None))
706707
}
707708

708709
#[test]

crates/bevy_asset/src/debug_asset_server.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
use bevy_app::{App, Plugin, Update};
66
use bevy_ecs::{prelude::*, system::SystemState};
77
use bevy_tasks::{IoTaskPool, TaskPoolBuilder};
8-
use bevy_utils::HashMap;
8+
use bevy_utils::{Duration, HashMap};
99
use std::{
1010
ops::{Deref, DerefMut},
1111
path::Path,
1212
};
1313

1414
use crate::{
15-
Asset, AssetEvent, AssetPlugin, AssetServer, Assets, FileAssetIo, Handle, HandleUntyped,
15+
Asset, AssetEvent, AssetPlugin, AssetServer, Assets, ChangeWatcher, FileAssetIo, Handle,
16+
HandleUntyped,
1617
};
1718

1819
/// A helper [`App`] used for hot reloading internal assets, which are compiled-in to Bevy plugins.
@@ -72,7 +73,7 @@ impl Plugin for DebugAssetServerPlugin {
7273
let mut debug_asset_app = App::new();
7374
debug_asset_app.add_plugin(AssetPlugin {
7475
asset_folder: "crates".to_string(),
75-
watch_for_changes: true,
76+
watch_for_changes: ChangeWatcher::with_delay(Duration::from_millis(200)),
7677
});
7778
app.insert_non_send_resource(DebugAssetApp(debug_asset_app));
7879
app.add_systems(Update, run_debug_asset_app);

crates/bevy_asset/src/filesystem_watcher.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
use bevy_utils::{default, HashMap, HashSet};
1+
use bevy_utils::{default, Duration, HashMap, HashSet};
22
use crossbeam_channel::Receiver;
33
use notify::{Event, RecommendedWatcher, RecursiveMode, Result, Watcher};
44
use std::path::{Path, PathBuf};
55

6+
use crate::ChangeWatcher;
7+
68
/// Watches for changes to files on the local filesystem.
79
///
810
/// When hot-reloading is enabled, the [`AssetServer`](crate::AssetServer) uses this to reload
@@ -11,10 +13,11 @@ pub struct FilesystemWatcher {
1113
pub watcher: RecommendedWatcher,
1214
pub receiver: Receiver<Result<Event>>,
1315
pub path_map: HashMap<PathBuf, HashSet<PathBuf>>,
16+
pub delay: Duration,
1417
}
1518

16-
impl Default for FilesystemWatcher {
17-
fn default() -> Self {
19+
impl FilesystemWatcher {
20+
pub fn new(configuration: &ChangeWatcher) -> Self {
1821
let (sender, receiver) = crossbeam_channel::unbounded();
1922
let watcher: RecommendedWatcher = RecommendedWatcher::new(
2023
move |res| {
@@ -27,11 +30,10 @@ impl Default for FilesystemWatcher {
2730
watcher,
2831
receiver,
2932
path_map: default(),
33+
delay: configuration.delay,
3034
}
3135
}
32-
}
3336

34-
impl FilesystemWatcher {
3537
/// Watch for changes recursively at the provided path.
3638
pub fn watch<P: AsRef<Path>>(&mut self, to_watch: P, to_reload: PathBuf) -> Result<()> {
3739
self.path_map

crates/bevy_asset/src/io/android_asset_io.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{AssetIo, AssetIoError, Metadata};
1+
use crate::{AssetIo, AssetIoError, ChangeWatcher, Metadata};
22
use anyhow::Result;
33
use bevy_utils::BoxedFuture;
44
use std::{
@@ -59,7 +59,7 @@ impl AssetIo for AndroidAssetIo {
5959
Ok(())
6060
}
6161

62-
fn watch_for_changes(&self) -> Result<(), AssetIoError> {
62+
fn watch_for_changes(&self, _configuration: &ChangeWatcher) -> Result<(), AssetIoError> {
6363
bevy_log::warn!("Watching for changes is not supported on Android");
6464
Ok(())
6565
}

crates/bevy_asset/src/io/file_asset_io.rs

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
#[cfg(feature = "filesystem_watcher")]
22
use crate::{filesystem_watcher::FilesystemWatcher, AssetServer};
3-
use crate::{AssetIo, AssetIoError, Metadata};
3+
use crate::{AssetIo, AssetIoError, ChangeWatcher, Metadata};
44
use anyhow::Result;
55
#[cfg(feature = "filesystem_watcher")]
6-
use bevy_ecs::system::Res;
6+
use bevy_ecs::system::{Local, Res};
77
use bevy_utils::BoxedFuture;
88
#[cfg(feature = "filesystem_watcher")]
9-
use bevy_utils::{default, HashSet};
9+
use bevy_utils::{default, HashMap, Instant};
1010
#[cfg(feature = "filesystem_watcher")]
1111
use crossbeam_channel::TryRecvError;
1212
use fs::File;
@@ -35,13 +35,13 @@ impl FileAssetIo {
3535
/// watching for changes.
3636
///
3737
/// See `get_base_path` below.
38-
pub fn new<P: AsRef<Path>>(path: P, watch_for_changes: bool) -> Self {
38+
pub fn new<P: AsRef<Path>>(path: P, watch_for_changes: &Option<ChangeWatcher>) -> Self {
3939
let file_asset_io = FileAssetIo {
4040
#[cfg(feature = "filesystem_watcher")]
4141
filesystem_watcher: default(),
4242
root_path: Self::get_base_path().join(path.as_ref()),
4343
};
44-
if watch_for_changes {
44+
if let Some(configuration) = watch_for_changes {
4545
#[cfg(any(
4646
not(feature = "filesystem_watcher"),
4747
target_arch = "wasm32",
@@ -52,7 +52,7 @@ impl FileAssetIo {
5252
wasm32 / android targets"
5353
);
5454
#[cfg(feature = "filesystem_watcher")]
55-
file_asset_io.watch_for_changes().unwrap();
55+
file_asset_io.watch_for_changes(configuration).unwrap();
5656
}
5757
file_asset_io
5858
}
@@ -143,10 +143,10 @@ impl AssetIo for FileAssetIo {
143143
Ok(())
144144
}
145145

146-
fn watch_for_changes(&self) -> Result<(), AssetIoError> {
146+
fn watch_for_changes(&self, configuration: &ChangeWatcher) -> Result<(), AssetIoError> {
147147
#[cfg(feature = "filesystem_watcher")]
148148
{
149-
*self.filesystem_watcher.write() = Some(default());
149+
*self.filesystem_watcher.write() = Some(FilesystemWatcher::new(configuration));
150150
}
151151
#[cfg(not(feature = "filesystem_watcher"))]
152152
bevy_log::warn!("Watching for changes is not supported when the `filesystem_watcher` feature is disabled");
@@ -174,22 +174,26 @@ impl AssetIo for FileAssetIo {
174174
feature = "filesystem_watcher",
175175
all(not(target_arch = "wasm32"), not(target_os = "android"))
176176
))]
177-
pub fn filesystem_watcher_system(asset_server: Res<AssetServer>) {
177+
pub fn filesystem_watcher_system(
178+
asset_server: Res<AssetServer>,
179+
mut changed: Local<HashMap<PathBuf, Instant>>,
180+
) {
178181
let asset_io =
179182
if let Some(asset_io) = asset_server.server.asset_io.downcast_ref::<FileAssetIo>() {
180183
asset_io
181184
} else {
182185
return;
183186
};
184187
let watcher = asset_io.filesystem_watcher.read();
188+
185189
if let Some(ref watcher) = *watcher {
186-
let mut changed = HashSet::<&PathBuf>::default();
187190
loop {
188191
let event = match watcher.receiver.try_recv() {
189192
Ok(result) => result.unwrap(),
190193
Err(TryRecvError::Empty) => break,
191194
Err(TryRecvError::Disconnected) => panic!("FilesystemWatcher disconnected."),
192195
};
196+
193197
if let notify::event::Event {
194198
kind: notify::event::EventKind::Modify(_),
195199
paths,
@@ -199,13 +203,22 @@ pub fn filesystem_watcher_system(asset_server: Res<AssetServer>) {
199203
for path in &paths {
200204
let Some(set) = watcher.path_map.get(path) else {continue};
201205
for to_reload in set {
202-
if !changed.contains(to_reload) {
203-
changed.insert(to_reload);
204-
let _ = asset_server.load_untracked(to_reload.as_path().into(), true);
205-
}
206+
// When an asset is modified, note down the timestamp (overriding any previous modification events)
207+
changed.insert(to_reload.to_owned(), Instant::now());
206208
}
207209
}
208210
}
209211
}
212+
213+
// Reload all assets whose last modification was at least 50ms ago.
214+
//
215+
// When changing and then saving a shader, several modification events are sent in short succession.
216+
// Unless we wait until we are sure the shader is finished being modified (and that there will be no more events coming),
217+
// we will sometimes get a crash when trying to reload a partially-modified shader.
218+
for (to_reload, _) in
219+
changed.drain_filter(|_, last_modified| last_modified.elapsed() >= watcher.delay)
220+
{
221+
let _ = asset_server.load_untracked(to_reload.as_path().into(), true);
222+
}
210223
}
211224
}

crates/bevy_asset/src/io/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ use std::{
2525
};
2626
use thiserror::Error;
2727

28+
use crate::ChangeWatcher;
29+
2830
/// Errors that occur while loading assets.
2931
#[derive(Error, Debug)]
3032
pub enum AssetIoError {
@@ -81,7 +83,7 @@ pub trait AssetIo: Downcast + Send + Sync + 'static {
8183
) -> Result<(), AssetIoError>;
8284

8385
/// Enables change tracking in this asset I/O.
84-
fn watch_for_changes(&self) -> Result<(), AssetIoError>;
86+
fn watch_for_changes(&self, configuration: &ChangeWatcher) -> Result<(), AssetIoError>;
8587

8688
/// Returns `true` if the path is a directory.
8789
fn is_dir(&self, path: &Path) -> bool {

crates/bevy_asset/src/io/wasm_asset_io.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{AssetIo, AssetIoError, Metadata};
1+
use crate::{AssetIo, AssetIoError, ChangeWatcher, Metadata};
22
use anyhow::Result;
33
use bevy_utils::BoxedFuture;
44
use js_sys::Uint8Array;
@@ -64,7 +64,7 @@ impl AssetIo for WasmAssetIo {
6464
Ok(())
6565
}
6666

67-
fn watch_for_changes(&self) -> Result<(), AssetIoError> {
67+
fn watch_for_changes(&self, _configuration: &ChangeWatcher) -> Result<(), AssetIoError> {
6868
bevy_log::warn!("Watching for changes is not supported in WASM");
6969
Ok(())
7070
}

crates/bevy_asset/src/lib.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ pub use reflect::*;
4949

5050
use bevy_app::{prelude::*, MainScheduleOrder};
5151
use bevy_ecs::schedule::ScheduleLabel;
52+
use bevy_utils::Duration;
5253

5354
/// Asset storages are updated.
5455
#[derive(Debug, Hash, PartialEq, Eq, Clone, ScheduleLabel)]
@@ -57,6 +58,30 @@ pub struct LoadAssets;
5758
#[derive(Debug, Hash, PartialEq, Eq, Clone, ScheduleLabel)]
5859
pub struct AssetEvents;
5960

61+
/// Configuration for hot reloading assets by watching for changes.
62+
#[derive(Debug, Clone)]
63+
pub struct ChangeWatcher {
64+
/// Minimum delay after which a file change will trigger a reload.
65+
///
66+
/// The change watcher will wait for this duration after a file change before reloading the
67+
/// asset. This is useful to avoid reloading an asset multiple times when it is changed
68+
/// multiple times in a short period of time, or to avoid reloading an asset that is still
69+
/// being written to.
70+
///
71+
/// If you have a slow hard drive or expect to reload large assets, you may want to increase
72+
/// this value.
73+
pub delay: Duration,
74+
}
75+
76+
impl ChangeWatcher {
77+
/// Enable change watching with the given delay when a file is changed.
78+
///
79+
/// See [`Self::delay`] for more details on how this value is used.
80+
pub fn with_delay(delay: Duration) -> Option<Self> {
81+
Some(Self { delay })
82+
}
83+
}
84+
6085
/// Adds support for [`Assets`] to an App.
6186
///
6287
/// Assets are typed collections with change tracking, which are added as App Resources. Examples of
@@ -67,14 +92,14 @@ pub struct AssetPlugin {
6792
pub asset_folder: String,
6893
/// Whether to watch for changes in asset files. Requires the `filesystem_watcher` feature,
6994
/// and cannot be supported on the wasm32 arch nor android os.
70-
pub watch_for_changes: bool,
95+
pub watch_for_changes: Option<ChangeWatcher>,
7196
}
7297

7398
impl Default for AssetPlugin {
7499
fn default() -> Self {
75100
Self {
76101
asset_folder: "assets".to_string(),
77-
watch_for_changes: false,
102+
watch_for_changes: None,
78103
}
79104
}
80105
}
@@ -86,7 +111,7 @@ impl AssetPlugin {
86111
/// delegate to the default `AssetIo` for the platform.
87112
pub fn create_platform_default_asset_io(&self) -> Box<dyn AssetIo> {
88113
#[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
89-
let source = FileAssetIo::new(&self.asset_folder, self.watch_for_changes);
114+
let source = FileAssetIo::new(&self.asset_folder, &self.watch_for_changes);
90115
#[cfg(target_arch = "wasm32")]
91116
let source = WasmAssetIo::new(&self.asset_folder);
92117
#[cfg(target_os = "android")]

examples/asset/custom_asset_io.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//! It does not know anything about the asset formats, only how to talk to the underlying storage.
44
55
use bevy::{
6-
asset::{AssetIo, AssetIoError, Metadata},
6+
asset::{AssetIo, AssetIoError, ChangeWatcher, Metadata},
77
prelude::*,
88
utils::BoxedFuture,
99
};
@@ -39,9 +39,9 @@ impl AssetIo for CustomAssetIo {
3939
self.0.watch_path_for_changes(to_watch, to_reload)
4040
}
4141

42-
fn watch_for_changes(&self) -> Result<(), AssetIoError> {
42+
fn watch_for_changes(&self, configuration: &ChangeWatcher) -> Result<(), AssetIoError> {
4343
info!("watch_for_changes()");
44-
self.0.watch_for_changes()
44+
self.0.watch_for_changes(configuration)
4545
}
4646

4747
fn get_metadata(&self, path: &Path) -> Result<Metadata, AssetIoError> {

examples/asset/hot_asset_reloading.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
//! running. This lets you immediately see the results of your changes without restarting the game.
33
//! This example illustrates hot reloading mesh changes.
44
5-
use bevy::prelude::*;
5+
use bevy::{asset::ChangeWatcher, prelude::*, utils::Duration};
66

77
fn main() {
88
App::new()
99
.add_plugins(DefaultPlugins.set(AssetPlugin {
1010
// Tell the asset server to watch for asset changes on disk:
11-
watch_for_changes: true,
11+
watch_for_changes: ChangeWatcher::with_delay(Duration::from_millis(200)),
1212
..default()
1313
}))
1414
.add_systems(Startup, setup)

examples/scene/scene.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
//! This example illustrates loading scenes from files.
2-
use bevy::{prelude::*, tasks::IoTaskPool, utils::Duration};
2+
use bevy::{asset::ChangeWatcher, prelude::*, tasks::IoTaskPool, utils::Duration};
33
use std::{fs::File, io::Write};
44

55
fn main() {
66
App::new()
77
.add_plugins(DefaultPlugins.set(AssetPlugin {
88
// This tells the AssetServer to watch for changes to assets.
99
// It enables our scenes to automatically reload in game when we modify their files.
10-
watch_for_changes: true,
10+
watch_for_changes: ChangeWatcher::with_delay(Duration::from_millis(200)),
1111
..default()
1212
}))
1313
.register_type::<ComponentA>()

examples/shader/post_processing.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
//! This is a fairly low level example and assumes some familiarity with rendering concepts and wgpu.
77
88
use bevy::{
9+
asset::ChangeWatcher,
910
core_pipeline::{
1011
clear_color::ClearColorConfig, core_3d,
1112
fullscreen_vertex_shader::fullscreen_shader_vertex_state,
@@ -29,13 +30,14 @@ use bevy::{
2930
view::{ExtractedView, ViewTarget},
3031
RenderApp,
3132
},
33+
utils::Duration,
3234
};
3335

3436
fn main() {
3537
App::new()
3638
.add_plugins(DefaultPlugins.set(AssetPlugin {
3739
// Hot reloading the shader works correctly
38-
watch_for_changes: true,
40+
watch_for_changes: ChangeWatcher::with_delay(Duration::from_millis(200)),
3941
..default()
4042
}))
4143
.add_plugin(PostProcessPlugin)

0 commit comments

Comments
 (0)