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}