Files
fyroxengine--fyrox/fyrox-impl/src/engine/task.rs
T
2026-07-13 12:24:51 +08:00

263 lines
11 KiB
Rust

// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//! Asynchronous task handler. See [`TaskPoolHandler`] for more info and usage examples.
use crate::plugin::error::GameResult;
use crate::plugin::PluginContainer;
use crate::{
core::{
pool::Handle,
task::{AsyncTask, AsyncTaskResult, TaskPool},
uuid::Uuid,
},
plugin::{Plugin, PluginContext},
scene::{node::Node, Scene},
script::{ScriptContext, ScriptTrait},
};
use fxhash::FxHashMap;
use std::sync::Arc;
pub(crate) type NodeTaskHandlerClosure = Box<
dyn for<'a, 'b, 'c> FnOnce(
Box<dyn AsyncTaskResult>,
&mut dyn ScriptTrait,
&mut ScriptContext<'a, 'b, 'c>,
) -> GameResult,
>;
pub(crate) type PluginTaskHandler = Box<
dyn for<'a, 'b> FnOnce(
Box<dyn AsyncTaskResult>,
&'a mut [PluginContainer],
&mut PluginContext<'a, 'b>,
) -> GameResult,
>;
pub(crate) struct NodeTaskHandler {
pub(crate) scene_handle: Handle<Scene>,
pub(crate) node_handle: Handle<Node>,
pub(crate) script_index: usize,
pub(crate) closure: NodeTaskHandlerClosure,
}
/// Asynchronous task handler is used as an executor for async functions (tasks), that in addition to them
/// has a closure, that will be called when a task is finished. The main use case for such tasks is to
/// off-thread a heavy task to one of background threads (from a thread pool on PC, or a microtask on
/// WebAssembly) and when it is done, incorporate its result in your game's state. A task and its
/// "on-complete" closure could be pretty much anything: procedural world generation + adding a
/// generated scene to the engine, asset loading + its instantiation, etc. It should be noted that
/// a task itself is executed asynchronously (in other thread), while a closure - synchronously,
/// just at the beginning of the next game loop iteration. This means, that you should never put
/// heavy tasks into the closure, otherwise it will result in quite notable stutters.
///
/// There are two main methods - [`TaskPoolHandler::spawn_plugin_task`] and [`TaskPoolHandler::spawn_script_task`].
/// They are somewhat similar, but the main difference between them is that the first one operates
/// on per plugin basis and the latter operates on scene node basis. This means that in case of
/// [`TaskPoolHandler::spawn_plugin_task`], it will accept an async task and when it is finished, it
/// will give you a result of the task and access to the plugin from which it was called, so you can
/// do some actions with the result. [`TaskPoolHandler::spawn_script_task`] does somewhat the same, but
/// on a scene node basis - when a task is done, the "on-complete" closure will be provided with a
/// wide context, allowing you to modify the caller's node state. See the docs for the respective
/// methods for more info.
pub struct TaskPoolHandler {
task_pool: Arc<TaskPool>,
plugin_task_handlers: FxHashMap<Uuid, PluginTaskHandler>,
node_task_handlers: FxHashMap<Uuid, NodeTaskHandler>,
}
impl TaskPoolHandler {
pub(crate) fn new(task_pool: Arc<TaskPool>) -> Self {
Self {
task_pool,
plugin_task_handlers: Default::default(),
node_task_handlers: Default::default(),
}
}
/// Spawns a task represented by the `future`, that does something and then adds the result to
/// a plugin it was called from using the `on_complete` closure.
///
/// ## Example
///
/// ```rust ,no_run
/// # use fyrox_impl::plugin::{Plugin, PluginContext, error::GameResult};
/// # use fyrox_impl::core::visitor::prelude::*;
/// # use fyrox_impl::core::reflect::prelude::*;
/// # use std::{fs::File, io::Read};
///
/// #[derive(Visit, Reflect, PartialEq, Debug)]
/// #[reflect(non_cloneable, type_uuid = "a7ba17c1-6104-4938-9ba7-ba479acf716a")]
/// struct MyGame {
/// data: Option<Vec<u8>>,
/// }
///
/// impl MyGame {
/// pub fn new(context: PluginContext) -> Self {
/// context.task_pool.spawn_plugin_task(
/// // Emulate heavy task by reading a potentially large file. The game will be fully
/// // responsive while it runs.
/// async move {
/// let mut file = File::open("some/file.txt").unwrap();
/// let mut data = Vec::new();
/// file.read_to_end(&mut data).unwrap();
/// data
/// },
/// // This closure is called when the future above has finished, but not immediately - on
/// // the next update iteration.
/// |data, game: &mut MyGame, _context| {
/// // Store the data in the game instance.
/// game.data = Some(data);
/// Ok(())
/// },
/// );
///
/// // Immediately return the new game instance with empty data.
/// Self { data: None }
/// }
/// }
///
/// impl Plugin for MyGame {
/// fn update(&mut self, _context: &mut PluginContext) -> GameResult {
/// // Do something with the data.
/// if let Some(data) = self.data.take() {
/// println!("The data is: {:?}", data);
/// }
/// Ok(())
/// }
/// }
/// ```
#[inline]
pub fn spawn_plugin_task<F, T, P, C>(&mut self, future: F, on_complete: C)
where
F: AsyncTask<T>,
T: AsyncTaskResult,
P: Plugin,
for<'a, 'b> C: FnOnce(T, &mut P, &mut PluginContext<'a, 'b>) -> GameResult + 'static,
{
let task_id = self.task_pool.spawn_with_result(future);
self.plugin_task_handlers.insert(
task_id,
Box::new(move |result, plugins, context| {
let plugin = plugins
.iter_mut()
.find_map(|p| p.cast_mut::<P>())
.expect("Plugin must be present!");
let typed = result.downcast::<T>().expect("Types must match!");
on_complete(*typed, plugin, context)
}),
);
}
/// Spawns a task represented by the `future`, that does something and then adds the result to
/// a scene node's script using the `on_complete` closure. This method could be used to off-thread some
/// heavy work from usual update routine (for example - pathfinding).
///
/// ## Examples
///
/// ```rust ,no_run
/// # use fyrox_impl::{
/// # core::{reflect::prelude::*, uuid::Uuid, visitor::prelude::*},
/// # resource::model::{Model, ModelResourceExtension},
/// # script::{ScriptContext, ScriptTrait},
/// # };
/// #
/// # use fyrox_impl::plugin::error::GameResult;
/// #
/// #[derive(Reflect, Visit, PartialEq, Default, Debug, Clone)]
/// #[reflect(type_uuid = "f5ded79e-6101-4e23-b20d-48cbdb25d87a")]
/// struct MyScript;
///
/// impl ScriptTrait for MyScript {
/// fn on_start(&mut self, ctx: &mut ScriptContext) -> GameResult {
/// ctx.task_pool.spawn_script_task(
/// ctx.scene_handle,
/// ctx.handle,
/// ctx.script_index,
/// // Request loading of some heavy asset. It does not actually does the loading in the
/// // same routine, since asset loading itself is asynchronous, but we can't block the
/// // current thread on all support platforms to wait until the loading is done. So we
/// // have to use this approach to load assets on demand. Since every asset implements
/// // Future trait, it can be used directly as a future. Alternatively, you can use async
/// // move { } block here.
/// ctx.resource_manager.request::<Model>("path/to/model.fbx"),
/// // This closure will executed only when the upper future is done and only on the next
/// // update iteration.
/// |result, script: &mut MyScript, ctx| {
/// result?.instantiate(&mut ctx.scene);
/// Ok(())
/// },
/// );
/// Ok(())
/// }
/// }
/// ```
#[inline]
pub fn spawn_script_task<F, T, C, S>(
&mut self,
scene_handle: Handle<Scene>,
node_handle: Handle<Node>,
script_index: usize,
future: F,
on_complete: C,
) where
F: AsyncTask<T>,
T: AsyncTaskResult,
for<'a, 'b, 'c> C:
FnOnce(T, &mut S, &mut ScriptContext<'a, 'b, 'c>) -> GameResult + 'static,
S: ScriptTrait,
{
let task_id = self.task_pool.spawn_with_result(future);
self.node_task_handlers.insert(
task_id,
NodeTaskHandler {
scene_handle,
node_handle,
script_index,
closure: Box::new(move |result, script, context| {
let script = script
.as_any_ref_mut()
.downcast_mut::<S>()
.expect("Types must match");
let result = result.downcast::<T>().expect("Types must match");
on_complete(*result, script, context)
}),
},
);
}
/// Returns a reference to the underlying, low level task pool, that could be used to for special
/// cases.
#[inline]
pub fn inner(&self) -> &Arc<TaskPool> {
&self.task_pool
}
#[inline]
pub(crate) fn pop_plugin_task_handler(&mut self, id: Uuid) -> Option<PluginTaskHandler> {
self.plugin_task_handlers.remove(&id)
}
#[inline]
pub(crate) fn pop_node_task_handler(&mut self, id: Uuid) -> Option<NodeTaskHandler> {
self.node_task_handlers.remove(&id)
}
}