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}