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