#![doc = include_str!("../README.md")]
use std::{
error::Error,
sync::{Arc, Weak},
};
use bevy::{
app::{Last, Plugin},
asset::{AssetEvent, AssetEvents, AssetId, Assets, Handle, StrongHandle},
ecs::system::SystemParam,
log::warn,
prelude::{
AppTypeRegistry, EventReader, IntoSystemConfigs, Res, ResMut, Resource,
SystemSet, World,
},
scene::Scene,
tasks::{
block_on, futures_lite::future, tick_global_task_pools_on_main_thread,
AsyncComputeTaskPool, Task,
},
utils::{HashMap, HashSet},
};
pub struct ScenePostProcessPlugin;
impl Plugin for ScenePostProcessPlugin {
fn build(&self, app: &mut bevy::prelude::App) {
app
.init_resource::<RegisteredPostProcessActions>()
.init_resource::<ScenePostProcessTasks>()
.add_systems(
Last,
(
drop_unused_scenes,
watch_for_changed_unprocessed
.before(tick_global_task_pools_on_main_thread),
handle_finished_processing
.after(tick_global_task_pools_on_main_thread),
)
.in_set(PostProcessSet)
.chain()
.after(AssetEvents),
);
}
}
#[derive(SystemSet, PartialEq, Eq, Hash, Debug, Clone)]
pub struct PostProcessSet;
#[derive(SystemParam)]
pub struct ScenePostProcessor<'w> {
intermediate: ResMut<'w, RegisteredPostProcessActions>,
scenes: Res<'w, Assets<Scene>>,
}
impl ScenePostProcessor<'_> {
pub fn process(
&mut self,
scene: Handle<Scene>,
actions: Vec<Arc<ActionFunc>>,
) -> Handle<Scene> {
let processed_handle = self.scenes.reserve_handle();
let Handle::Strong(processed_strong_handle_arc) = &processed_handle else {
unreachable!("reserve_handle always returns a Handle::Strong");
};
if self.scenes.contains(&scene) {
self.intermediate.new_scenes.insert(scene.id());
}
let processing_targets = self
.intermediate
.unprocessed_to_targets
.entry(scene.id())
.or_insert_with(move || ProcessingTargets {
unprocessed_handle: scene,
processed_to_action: HashMap::new(),
});
processing_targets.processed_to_action.insert(
processed_handle.id(),
PostProcessAction {
processed_handle: Arc::downgrade(processed_strong_handle_arc),
actions,
},
);
processed_handle
}
}
pub type BoxedError = Box<dyn Error + Send + Sync>;
pub type ActionFunc =
dyn Fn(&mut World) -> Result<(), BoxedError> + Send + Sync;
#[derive(Resource, Default)]
struct RegisteredPostProcessActions {
unprocessed_to_targets: HashMap<AssetId<Scene>, ProcessingTargets>,
new_scenes: HashSet<AssetId<Scene>>,
}
struct ProcessingTargets {
unprocessed_handle: Handle<Scene>,
processed_to_action: HashMap<AssetId<Scene>, PostProcessAction>,
}
struct PostProcessAction {
processed_handle: Weak<StrongHandle>,
actions: Vec<Arc<ActionFunc>>,
}
#[derive(Resource, Default)]
struct ScenePostProcessTasks(
HashMap<PostProcessKey, Task<Result<Scene, BoxedError>>>,
);
#[derive(Debug, Hash, PartialEq, Eq)]
struct PostProcessKey {
unprocessed_id: AssetId<Scene>,
processed_id: AssetId<Scene>,
}
fn drop_unused_scenes(
mut intermediate: ResMut<RegisteredPostProcessActions>,
mut post_process_tasks: ResMut<ScenePostProcessTasks>,
) {
intermediate.unprocessed_to_targets.retain(|unprocessed_id, targets| {
targets.processed_to_action.retain(|processed_id, action| {
if action.processed_handle.strong_count() > 0 {
return true;
}
post_process_tasks.0.remove(&PostProcessKey {
unprocessed_id: *unprocessed_id,
processed_id: *processed_id,
});
false
});
!targets.processed_to_action.is_empty()
});
}
fn watch_for_changed_unprocessed(
mut scene_events: EventReader<AssetEvent<Scene>>,
mut intermediate: ResMut<RegisteredPostProcessActions>,
type_registry: Res<AppTypeRegistry>,
mut scenes: ResMut<Assets<Scene>>,
mut post_process_tasks: ResMut<ScenePostProcessTasks>,
) {
let intermediate = intermediate.as_mut();
'outer_loop: for unprocessed_id in scene_events
.read()
.filter_map(asset_event_to_change)
.chain(intermediate.new_scenes.drain())
.collect::<HashSet<_>>()
{
let Some(post_process_targets) =
intermediate.unprocessed_to_targets.get(&unprocessed_id)
else {
continue;
};
if !scenes.contains(unprocessed_id) {
for (&processed_id, action) in
post_process_targets.processed_to_action.iter()
{
if let Some(strong_handle) = action.processed_handle.upgrade() {
scenes.remove(&Handle::Strong(strong_handle));
}
post_process_tasks
.0
.remove(&PostProcessKey { unprocessed_id, processed_id });
}
continue;
}
let Some(unprocessed_scene) =
scenes.get(&post_process_targets.unprocessed_handle)
else {
continue;
};
for (&processed_id, action) in
post_process_targets.processed_to_action.iter()
{
let cloned_scene = match unprocessed_scene.clone_with(&type_registry) {
Err(error) => {
warn!("Failed to clone the unprocessed scene: {error}");
for &processed_id in post_process_targets.processed_to_action.keys() {
scenes.remove(processed_id);
}
continue 'outer_loop;
}
Ok(cloned_scene) => cloned_scene,
};
let async_actions = action.actions.clone();
let task_pool = AsyncComputeTaskPool::get();
let task = task_pool.spawn(async move {
let mut processed_scene = cloned_scene;
for action in async_actions {
action(&mut processed_scene.world)?;
}
Ok(processed_scene)
});
post_process_tasks
.0
.insert(PostProcessKey { unprocessed_id, processed_id }, task);
}
}
}
fn asset_event_to_change(
scene_event: &AssetEvent<Scene>,
) -> Option<AssetId<Scene>> {
match scene_event {
AssetEvent::Added { id }
| AssetEvent::Modified { id }
| AssetEvent::Removed { id } => Some(*id),
AssetEvent::Unused { .. } => None,
AssetEvent::LoadedWithDependencies { .. } => None,
}
}
fn handle_finished_processing(
mut post_process_tasks: ResMut<ScenePostProcessTasks>,
intermediate: Res<RegisteredPostProcessActions>,
mut scenes: ResMut<Assets<Scene>>,
) {
post_process_tasks.0.retain(|key, task| {
if !task.is_finished() {
return true;
}
let processed_scene =
block_on(future::poll_once(task)).expect("the task is finished");
let processed_scene = match processed_scene {
Err(error) => {
scenes.remove(key.processed_id);
warn!("Failed to process scene {:?}: {error}", key.unprocessed_id);
return false;
}
Ok(processed_scene) => processed_scene,
};
let Some(action) = intermediate
.unprocessed_to_targets
.get(&key.unprocessed_id)
.and_then(|targets| targets.processed_to_action.get(&key.processed_id))
else {
return false;
};
if let Some(strong_handle) = action.processed_handle.upgrade() {
scenes.insert(&Handle::Strong(strong_handle), processed_scene);
}
false
});
}
#[cfg(test)]
#[path = "lib_test.rs"]
mod test;