Skip to content

Commit

Permalink
Fix hot reloading for read_asset_bytes (bevyengine#6797)
Browse files Browse the repository at this point in the history
# Objective

Fixes bevyengine#6780

## Solution

- record the asset path of each watched file
- call `AssetIo::watch_for_changes` in `LoadContext::read_asset_bytes`

---

## Changelog

### Fixed
- fixed hot reloading for `LoadContext::read_asset_bytes`

### Changed
- `AssetIo::watch_path_for_changes` allows watched path and path to reload to differ

## Migration Guide
- for custom `AssetIo`s, differentiate paths to watch and asset paths to reload as a consequence

Co-authored-by: Vincent Junge <specificprotagonist@posteo.org>
  • Loading branch information
2 people authored and Shfty committed Mar 19, 2023
1 parent 3ee9638 commit 8b398b4
Show file tree
Hide file tree
Showing 8 changed files with 72 additions and 28 deletions.
2 changes: 1 addition & 1 deletion crates/bevy_asset/src/asset_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,7 @@ impl AssetServer {
}

self.asset_io()
.watch_path_for_changes(asset_path.path())
.watch_path_for_changes(asset_path.path(), None)
.unwrap();
self.create_assets_in_load_context(&mut load_context);
Ok(asset_path_id)
Expand Down
23 changes: 17 additions & 6 deletions crates/bevy_asset/src/filesystem_watcher.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use bevy_utils::{default, HashMap, HashSet};
use crossbeam_channel::Receiver;
use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Result, Watcher};
use std::path::Path;
use notify::{Event, RecommendedWatcher, RecursiveMode, Result, Watcher};
use std::path::{Path, PathBuf};

/// Watches for changes to files on the local filesystem.
///
Expand All @@ -9,6 +10,7 @@ use std::path::Path;
pub struct FilesystemWatcher {
pub watcher: RecommendedWatcher,
pub receiver: Receiver<Result<Event>>,
pub path_map: HashMap<PathBuf, HashSet<PathBuf>>,
}

impl Default for FilesystemWatcher {
Expand All @@ -18,16 +20,25 @@ impl Default for FilesystemWatcher {
move |res| {
sender.send(res).expect("Watch event send failure.");
},
Config::default(),
default(),
)
.expect("Failed to create filesystem watcher.");
FilesystemWatcher { watcher, receiver }
FilesystemWatcher {
watcher,
receiver,
path_map: default(),
}
}
}

impl FilesystemWatcher {
/// Watch for changes recursively at the provided path.
pub fn watch<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
self.watcher.watch(path.as_ref(), RecursiveMode::Recursive)
pub fn watch<P: AsRef<Path>>(&mut self, to_watch: P, to_reload: PathBuf) -> Result<()> {
self.path_map
.entry(to_watch.as_ref().to_owned())
.or_default()
.insert(to_reload);
self.watcher
.watch(to_watch.as_ref(), RecursiveMode::Recursive)
}
}
6 changes: 5 additions & 1 deletion crates/bevy_asset/src/io/android_asset_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ impl AssetIo for AndroidAssetIo {
Ok(Box::new(std::iter::empty::<PathBuf>()))
}

fn watch_path_for_changes(&self, _path: &Path) -> Result<(), AssetIoError> {
fn watch_path_for_changes(
&self,
_to_watch: &Path,
_to_reload: Option<PathBuf>,
) -> Result<(), AssetIoError> {
Ok(())
}

Expand Down
31 changes: 19 additions & 12 deletions crates/bevy_asset/src/io/file_asset_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use anyhow::Result;
use bevy_ecs::system::Res;
use bevy_utils::BoxedFuture;
#[cfg(feature = "filesystem_watcher")]
use bevy_utils::HashSet;
use bevy_utils::{default, HashSet};
#[cfg(feature = "filesystem_watcher")]
use crossbeam_channel::TryRecvError;
use fs::File;
Expand Down Expand Up @@ -38,7 +38,7 @@ impl FileAssetIo {
pub fn new<P: AsRef<Path>>(path: P, watch_for_changes: bool) -> Self {
let file_asset_io = FileAssetIo {
#[cfg(feature = "filesystem_watcher")]
filesystem_watcher: Default::default(),
filesystem_watcher: default(),
root_path: Self::get_base_path().join(path.as_ref()),
};
if watch_for_changes {
Expand Down Expand Up @@ -122,15 +122,20 @@ impl AssetIo for FileAssetIo {
)))
}

fn watch_path_for_changes(&self, _path: &Path) -> Result<(), AssetIoError> {
fn watch_path_for_changes(
&self,
to_watch: &Path,
to_reload: Option<PathBuf>,
) -> Result<(), AssetIoError> {
#[cfg(feature = "filesystem_watcher")]
{
let path = self.root_path.join(_path);
let to_reload = to_reload.unwrap_or_else(|| to_watch.to_owned());
let to_watch = self.root_path.join(to_watch);
let mut watcher = self.filesystem_watcher.write();
if let Some(ref mut watcher) = *watcher {
watcher
.watch(&path)
.map_err(|_error| AssetIoError::PathWatchError(path))?;
.watch(&to_watch, to_reload)
.map_err(|_error| AssetIoError::PathWatchError(to_watch))?;
}
}

Expand All @@ -140,7 +145,7 @@ impl AssetIo for FileAssetIo {
fn watch_for_changes(&self) -> Result<(), AssetIoError> {
#[cfg(feature = "filesystem_watcher")]
{
*self.filesystem_watcher.write() = Some(FilesystemWatcher::default());
*self.filesystem_watcher.write() = Some(default());
}
#[cfg(not(feature = "filesystem_watcher"))]
bevy_log::warn!("Watching for changes is not supported when the `filesystem_watcher` feature is disabled");
Expand Down Expand Up @@ -169,7 +174,6 @@ impl AssetIo for FileAssetIo {
all(not(target_arch = "wasm32"), not(target_os = "android"))
))]
pub fn filesystem_watcher_system(asset_server: Res<AssetServer>) {
let mut changed = HashSet::default();
let asset_io =
if let Some(asset_io) = asset_server.server.asset_io.downcast_ref::<FileAssetIo>() {
asset_io
Expand All @@ -178,6 +182,7 @@ pub fn filesystem_watcher_system(asset_server: Res<AssetServer>) {
};
let watcher = asset_io.filesystem_watcher.read();
if let Some(ref watcher) = *watcher {
let mut changed = HashSet::<&PathBuf>::default();
loop {
let event = match watcher.receiver.try_recv() {
Ok(result) => result.unwrap(),
Expand All @@ -191,12 +196,14 @@ pub fn filesystem_watcher_system(asset_server: Res<AssetServer>) {
} = event
{
for path in &paths {
if !changed.contains(path) {
let relative_path = path.strip_prefix(&asset_io.root_path).unwrap();
let _ = asset_server.load_untracked(relative_path.into(), true);
let Some(set) = watcher.path_map.get(path) else {continue};
for to_reload in set {
if !changed.contains(to_reload) {
changed.insert(to_reload);
let _ = asset_server.load_untracked(to_reload.as_path().into(), true);
}
}
}
changed.extend(paths);
}
}
}
Expand Down
14 changes: 13 additions & 1 deletion crates/bevy_asset/src/io/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,19 @@ pub trait AssetIo: Downcast + Send + Sync + 'static {
fn get_metadata(&self, path: &Path) -> Result<Metadata, AssetIoError>;

/// Tells the asset I/O to watch for changes recursively at the provided path.
fn watch_path_for_changes(&self, path: &Path) -> Result<(), AssetIoError>;
///
/// No-op if `watch_for_changes` hasn't been called yet.
/// Otherwise triggers a reload each time `to_watch` changes.
/// In most cases the asset found at the watched path should be changed,
/// but when an asset depends on data at another path, the asset's path
/// is provided in `to_reload`.
/// Note that there may be a many-to-many correspondence between
/// `to_watch` and `to_reload` paths.
fn watch_path_for_changes(
&self,
to_watch: &Path,
to_reload: Option<PathBuf>,
) -> Result<(), AssetIoError>;

/// Enables change tracking in this asset I/O.
fn watch_for_changes(&self) -> Result<(), AssetIoError>;
Expand Down
6 changes: 5 additions & 1 deletion crates/bevy_asset/src/io/wasm_asset_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@ impl AssetIo for WasmAssetIo {
Ok(Box::new(std::iter::empty::<PathBuf>()))
}

fn watch_path_for_changes(&self, _path: &Path) -> Result<(), AssetIoError> {
fn watch_path_for_changes(
&self,
_to_watch: &Path,
_to_reload: Option<PathBuf>,
) -> Result<(), AssetIoError> {
Ok(())
}

Expand Down
2 changes: 2 additions & 0 deletions crates/bevy_asset/src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ impl<'a> LoadContext<'a> {
/// Reads the contents of the file at the specified path through the [`AssetIo`] associated
/// with this context.
pub async fn read_asset_bytes<P: AsRef<Path>>(&self, path: P) -> Result<Vec<u8>, AssetIoError> {
self.asset_io
.watch_path_for_changes(path.as_ref(), Some(self.path.to_owned()))?;
self.asset_io.load_path(path.as_ref()).await
}

Expand Down
16 changes: 10 additions & 6 deletions examples/asset/custom_asset_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,25 @@ struct CustomAssetIo(Box<dyn AssetIo>);

impl AssetIo for CustomAssetIo {
fn load_path<'a>(&'a self, path: &'a Path) -> BoxedFuture<'a, Result<Vec<u8>, AssetIoError>> {
info!("load_path({:?})", path);
info!("load_path({path:?})");
self.0.load_path(path)
}

fn read_directory(
&self,
path: &Path,
) -> Result<Box<dyn Iterator<Item = PathBuf>>, AssetIoError> {
info!("read_directory({:?})", path);
info!("read_directory({path:?})");
self.0.read_directory(path)
}

fn watch_path_for_changes(&self, path: &Path) -> Result<(), AssetIoError> {
info!("watch_path_for_changes({:?})", path);
self.0.watch_path_for_changes(path)
fn watch_path_for_changes(
&self,
to_watch: &Path,
to_reload: Option<PathBuf>,
) -> Result<(), AssetIoError> {
info!("watch_path_for_changes({to_watch:?}, {to_reload:?})");
self.0.watch_path_for_changes(to_watch, to_reload)
}

fn watch_for_changes(&self) -> Result<(), AssetIoError> {
Expand All @@ -41,7 +45,7 @@ impl AssetIo for CustomAssetIo {
}

fn get_metadata(&self, path: &Path) -> Result<Metadata, AssetIoError> {
info!("get_metadata({:?})", path);
info!("get_metadata({path:?})");
self.0.get_metadata(path)
}
}
Expand Down

0 comments on commit 8b398b4

Please sign in to comment.