Skip to main content

fyrox_impl/engine/
task.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! Asynchronous task handler. See [`TaskPoolHandler`] for more info and usage examples.
22
23use crate::plugin::error::GameResult;
24use crate::plugin::PluginContainer;
25use crate::{
26    core::{
27        pool::Handle,
28        task::{AsyncTask, AsyncTaskResult, TaskPool},
29        uuid::Uuid,
30    },
31    plugin::{Plugin, PluginContext},
32    scene::{node::Node, Scene},
33    script::{ScriptContext, ScriptTrait},
34};
35use fxhash::FxHashMap;
36use std::sync::Arc;
37
38pub(crate) type NodeTaskHandlerClosure = Box<
39    dyn for<'a, 'b, 'c> Fn(
40        Box<dyn AsyncTaskResult>,
41        &mut dyn ScriptTrait,
42        &mut ScriptContext<'a, 'b, 'c>,
43    ) -> GameResult,
44>;
45
46pub(crate) type PluginTaskHandler = Box<
47    dyn for<'a, 'b> Fn(
48        Box<dyn AsyncTaskResult>,
49        &'a mut [PluginContainer],
50        &mut PluginContext<'a, 'b>,
51    ) -> GameResult,
52>;
53
54pub(crate) struct NodeTaskHandler {
55    pub(crate) scene_handle: Handle<Scene>,
56    pub(crate) node_handle: Handle<Node>,
57    pub(crate) script_index: usize,
58    pub(crate) closure: NodeTaskHandlerClosure,
59}
60
61/// Asynchronous task handler is used as an executor for async functions (tasks), that in addition to them
62/// has a closure, that will be called when a task is finished. The main use case for such tasks is to
63/// off-thread a heavy task to one of background threads (from a thread pool on PC, or a microtask on
64/// WebAssembly) and when it is done, incorporate its result in your game's state. A task and its
65/// "on-complete" closure could be pretty much anything: procedural world generation + adding a
66/// generated scene to the engine, asset loading + its instantiation, etc. It should be noted that
67/// a task itself is executed asynchronously (in other thread), while a closure - synchronously,
68/// just at the beginning of the next game loop iteration. This means, that you should never put
69/// heavy tasks into the closure, otherwise it will result in quite notable stutters.
70///
71/// There are two main methods - [`TaskPoolHandler::spawn_plugin_task`] and [`TaskPoolHandler::spawn_script_task`].
72/// They are somewhat similar, but the main difference between them is that the first one operates
73/// on per plugin basis and the latter operates on scene node basis. This means that in case of
74/// [`TaskPoolHandler::spawn_plugin_task`], it will accept an async task and when it is finished, it
75/// will give you a result of the task and access to the plugin from which it was called, so you can
76/// do some actions with the result. [`TaskPoolHandler::spawn_script_task`] does somewhat the same, but
77/// on a scene node basis - when a task is done, the "on-complete" closure will be provided with a
78/// wide context, allowing you to modify the caller's node state. See the docs for the respective
79/// methods for more info.
80pub struct TaskPoolHandler {
81    task_pool: Arc<TaskPool>,
82    plugin_task_handlers: FxHashMap<Uuid, PluginTaskHandler>,
83    node_task_handlers: FxHashMap<Uuid, NodeTaskHandler>,
84}
85
86impl TaskPoolHandler {
87    pub(crate) fn new(task_pool: Arc<TaskPool>) -> Self {
88        Self {
89            task_pool,
90            plugin_task_handlers: Default::default(),
91            node_task_handlers: Default::default(),
92        }
93    }
94
95    /// Spawns a task represented by the `future`, that does something and then adds the result to
96    /// a plugin it was called from using the `on_complete` closure.
97    ///
98    /// ## Example
99    ///
100    /// ```rust ,no_run
101    /// # use fyrox_impl::plugin::{Plugin, PluginContext, error::GameResult};
102    /// # use fyrox_impl::core::visitor::prelude::*;
103    /// # use fyrox_impl::core::reflect::prelude::*;
104    /// # use std::{fs::File, io::Read};
105    ///
106    /// #[derive(Visit, Reflect, Debug)]
107    /// #[reflect(non_cloneable)]
108    /// struct MyGame {
109    ///     data: Option<Vec<u8>>,
110    /// }
111    ///
112    /// impl MyGame {
113    ///     pub fn new(context: PluginContext) -> Self {
114    ///         context.task_pool.spawn_plugin_task(
115    ///             // Emulate heavy task by reading a potentially large file. The game will be fully
116    ///             // responsive while it runs.
117    ///             async move {
118    ///                 let mut file = File::open("some/file.txt").unwrap();
119    ///                 let mut data = Vec::new();
120    ///                 file.read_to_end(&mut data).unwrap();
121    ///                 data
122    ///             },
123    ///             // This closure is called when the future above has finished, but not immediately - on
124    ///             // the next update iteration.
125    ///             |data, game: &mut MyGame, _context| {
126    ///                 // Store the data in the game instance.
127    ///                 game.data = Some(data);
128    ///                 Ok(())
129    ///             },
130    ///         );
131    ///
132    ///         // Immediately return the new game instance with empty data.
133    ///         Self { data: None }
134    ///     }
135    /// }
136    ///
137    /// impl Plugin for MyGame {
138    ///     fn update(&mut self, _context: &mut PluginContext) -> GameResult {
139    ///         // Do something with the data.
140    ///         if let Some(data) = self.data.take() {
141    ///             println!("The data is: {:?}", data);
142    ///         }
143    ///         Ok(())
144    ///     }
145    /// }
146    /// ```
147    #[inline]
148    pub fn spawn_plugin_task<F, T, P, C>(&mut self, future: F, on_complete: C)
149    where
150        F: AsyncTask<T>,
151        T: AsyncTaskResult,
152        P: Plugin,
153        for<'a, 'b> C: Fn(T, &mut P, &mut PluginContext<'a, 'b>) -> GameResult + 'static,
154    {
155        let task_id = self.task_pool.spawn_with_result(future);
156        self.plugin_task_handlers.insert(
157            task_id,
158            Box::new(move |result, plugins, context| {
159                let plugin = plugins
160                    .iter_mut()
161                    .find_map(|p| p.cast_mut::<P>())
162                    .expect("Plugin must be present!");
163                let typed = result.downcast::<T>().expect("Types must match!");
164                on_complete(*typed, plugin, context)
165            }),
166        );
167    }
168
169    /// Spawns a task represented by the `future`, that does something and then adds the result to
170    /// a scene node's script using the `on_complete` closure. This method could be used to off-thread some
171    /// heavy work from usual update routine (for example - pathfinding).
172    ///
173    /// ## Examples
174    ///
175    /// ```rust ,no_run
176    /// # use fyrox_impl::{
177    /// #     core::{reflect::prelude::*, uuid::Uuid, visitor::prelude::*, impl_component_provider},
178    /// #     resource::model::{Model, ModelResourceExtension},
179    /// #     script::{ScriptContext, ScriptTrait},
180    /// # };
181    /// # use fyrox_core::uuid_provider;
182    /// # use fyrox_impl::plugin::error::GameResult;
183    /// #
184    /// #[derive(Reflect, Visit, Default, Debug, Clone)]
185    /// struct MyScript;
186    ///
187    /// # impl_component_provider!(MyScript);
188    /// # uuid_provider!(MyScript = "f5ded79e-6101-4e23-b20d-48cbdb25d87a");
189    ///
190    /// impl ScriptTrait for MyScript {
191    ///     fn on_start(&mut self, ctx: &mut ScriptContext) -> GameResult {
192    ///         ctx.task_pool.spawn_script_task(
193    ///             ctx.scene_handle,
194    ///             ctx.handle,
195    ///             ctx.script_index,
196    ///             // Request loading of some heavy asset. It does not actually does the loading in the
197    ///             // same routine, since asset loading itself is asynchronous, but we can't block the
198    ///             // current thread on all support platforms to wait until the loading is done. So we
199    ///             // have to use this approach to load assets on demand. Since every asset implements
200    ///             // Future trait, it can be used directly as a future. Alternatively, you can use async
201    ///             // move { } block here.
202    ///             ctx.resource_manager.request::<Model>("path/to/model.fbx"),
203    ///             // This closure will executed only when the upper future is done and only on the next
204    ///             // update iteration.
205    ///             |result, script: &mut MyScript, ctx| {
206    ///                 result?.instantiate(&mut ctx.scene);
207    ///                 Ok(())
208    ///             },
209    ///         );
210    ///         Ok(())
211    ///     }
212    /// }
213    /// ```
214    #[inline]
215    pub fn spawn_script_task<F, T, C, S>(
216        &mut self,
217        scene_handle: Handle<Scene>,
218        node_handle: Handle<Node>,
219        script_index: usize,
220        future: F,
221        on_complete: C,
222    ) where
223        F: AsyncTask<T>,
224        T: AsyncTaskResult,
225        for<'a, 'b, 'c> C: Fn(T, &mut S, &mut ScriptContext<'a, 'b, 'c>) -> GameResult + 'static,
226        S: ScriptTrait,
227    {
228        let task_id = self.task_pool.spawn_with_result(future);
229        self.node_task_handlers.insert(
230            task_id,
231            NodeTaskHandler {
232                scene_handle,
233                node_handle,
234                script_index,
235                closure: Box::new(move |result, script, context| {
236                    let script = script
237                        .as_any_ref_mut()
238                        .downcast_mut::<S>()
239                        .expect("Types must match");
240                    let result = result.downcast::<T>().expect("Types must match");
241                    on_complete(*result, script, context)
242                }),
243            },
244        );
245    }
246
247    /// Returns a reference to the underlying, low level task pool, that could be used to for special
248    /// cases.
249    #[inline]
250    pub fn inner(&self) -> &Arc<TaskPool> {
251        &self.task_pool
252    }
253
254    #[inline]
255    pub(crate) fn pop_plugin_task_handler(&mut self, id: Uuid) -> Option<PluginTaskHandler> {
256        self.plugin_task_handlers.remove(&id)
257    }
258
259    #[inline]
260    pub(crate) fn pop_node_task_handler(&mut self, id: Uuid) -> Option<NodeTaskHandler> {
261        self.node_task_handlers.remove(&id)
262    }
263}