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    /// struct MyGame {
107    ///     data: Option<Vec<u8>>,
108    /// }
109    ///
110    /// impl MyGame {
111    ///     pub fn new(context: PluginContext) -> Self {
112    ///         context.task_pool.spawn_plugin_task(
113    ///             // Emulate heavy task by reading a potentially large file. The game will be fully
114    ///             // responsive while it runs.
115    ///             async move {
116    ///                 let mut file = File::open("some/file.txt").unwrap();
117    ///                 let mut data = Vec::new();
118    ///                 file.read_to_end(&mut data).unwrap();
119    ///                 data
120    ///             },
121    ///             // This closure is called when the future above has finished, but not immediately - on
122    ///             // the next update iteration.
123    ///             |data, game: &mut MyGame, _context| {
124    ///                 // Store the data in the game instance.
125    ///                 game.data = Some(data);
126    ///             },
127    ///         );
128    ///
129    ///         // Immediately return the new game instance with empty data.
130    ///         Self { data: None }
131    ///     }
132    /// }
133    ///
134    /// impl Plugin for MyGame {
135    ///     fn update(&mut self, _context: &mut PluginContext) {
136    ///         // Do something with the data.
137    ///         if let Some(data) = self.data.take() {
138    ///             println!("The data is: {:?}", data);
139    ///         }
140    ///     }
141    /// }
142    /// ```
143    #[inline]
144    pub fn spawn_plugin_task<F, T, P, C>(&mut self, future: F, on_complete: C)
145    where
146        F: AsyncTask<T>,
147        T: AsyncTaskResult,
148        P: Plugin,
149        for<'a, 'b> C: Fn(T, &mut P, &mut PluginContext<'a, 'b>) + 'static,
150    {
151        let task_id = self.task_pool.spawn_with_result(future);
152        self.plugin_task_handlers.insert(
153            task_id,
154            Box::new(move |result, plugins, context| {
155                let plugin = plugins
156                    .iter_mut()
157                    .find_map(|p| p.cast_mut::<P>())
158                    .expect("Plugin must be present!");
159                let typed = result.downcast::<T>().expect("Types must match!");
160                on_complete(*typed, plugin, context)
161            }),
162        );
163    }
164
165    /// Spawns a task represented by the `future`, that does something and then adds the result to
166    /// a scene node's script using the `on_complete` closure. This method could be used to off-thread some
167    /// heavy work from usual update routine (for example - pathfinding).
168    ///
169    /// ## Examples
170    ///
171    /// ```rust ,no_run
172    /// # use fyrox_impl::{
173    /// #     core::{reflect::prelude::*, uuid::Uuid, visitor::prelude::*, impl_component_provider},
174    /// #     resource::model::{Model, ModelResourceExtension},
175    /// #     script::{ScriptContext, ScriptTrait},
176    /// # };
177    /// # use fyrox_core::uuid_provider;
178    /// #
179    /// #[derive(Reflect, Visit, Default, Debug, Clone)]
180    /// struct MyScript;
181    ///
182    /// # impl_component_provider!(MyScript);
183    /// # uuid_provider!(MyScript = "f5ded79e-6101-4e23-b20d-48cbdb25d87a");
184    ///
185    /// impl ScriptTrait for MyScript {
186    ///     fn on_start(&mut self, ctx: &mut ScriptContext) {
187    ///         ctx.task_pool.spawn_script_task(
188    ///             ctx.scene_handle,
189    ///             ctx.handle,
190    ///             ctx.script_index,
191    ///             // Request loading of some heavy asset. It does not actually does the loading in the
192    ///             // same routine, since asset loading itself is asynchronous, but we can't block the
193    ///             // current thread on all support platforms to wait until the loading is done. So we
194    ///             // have to use this approach to load assets on demand. Since every asset implements
195    ///             // Future trait, it can be used directly as a future. Alternatively, you can use async
196    ///             // move { } block here.
197    ///             ctx.resource_manager.request::<Model>("path/to/model.fbx"),
198    ///             // This closure will executed only when the upper future is done and only on the next
199    ///             // update iteration.
200    ///             |result, script: &mut MyScript, ctx| {
201    ///                 if let Ok(model) = result {
202    ///                     model.instantiate(&mut ctx.scene);
203    ///                 }
204    ///             },
205    ///         );
206    ///     }
207    /// }
208    /// ```
209    #[inline]
210    pub fn spawn_script_task<F, T, C, S>(
211        &mut self,
212        scene_handle: Handle<Scene>,
213        node_handle: Handle<Node>,
214        script_index: usize,
215        future: F,
216        on_complete: C,
217    ) where
218        F: AsyncTask<T>,
219        T: AsyncTaskResult,
220        for<'a, 'b, 'c> C: Fn(T, &mut S, &mut ScriptContext<'a, 'b, 'c>) + 'static,
221        S: ScriptTrait,
222    {
223        let task_id = self.task_pool.spawn_with_result(future);
224        self.node_task_handlers.insert(
225            task_id,
226            NodeTaskHandler {
227                scene_handle,
228                node_handle,
229                script_index,
230                closure: Box::new(move |result, script, context| {
231                    let script = script
232                        .as_any_ref_mut()
233                        .downcast_mut::<S>()
234                        .expect("Types must match");
235                    let result = result.downcast::<T>().expect("Types must match");
236                    on_complete(*result, script, context);
237                }),
238            },
239        );
240    }
241
242    /// Returns a reference to the underlying, low level task pool, that could be used to for special
243    /// cases.
244    #[inline]
245    pub fn inner(&self) -> &Arc<TaskPool> {
246        &self.task_pool
247    }
248
249    #[inline]
250    pub(crate) fn pop_plugin_task_handler(&mut self, id: Uuid) -> Option<PluginTaskHandler> {
251        self.plugin_task_handlers.remove(&id)
252    }
253
254    #[inline]
255    pub(crate) fn pop_node_task_handler(&mut self, id: Uuid) -> Option<NodeTaskHandler> {
256        self.node_task_handlers.remove(&id)
257    }
258}