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}