Skip to main content

goud_engine/assets/server/
async_operations.rs

1//! Non-blocking asset loading operations (native-only).
2//!
3//! Uses a rayon thread pool for background file I/O and parsing, with results
4//! communicated back to the main thread via `std::sync::mpsc` channels.
5
6#[cfg(all(feature = "native", not(feature = "web")))]
7use super::core::AssetServer;
8#[cfg(all(feature = "native", not(feature = "web")))]
9use super::core::LoadResult;
10#[cfg(all(feature = "native", not(feature = "web")))]
11use crate::assets::{Asset, AssetHandle, AssetId, AssetLoadError, AssetPath, LoadContext};
12#[cfg(all(feature = "native", not(feature = "web")))]
13use std::path::Path;
14
15#[cfg(all(feature = "native", not(feature = "web")))]
16impl AssetServer {
17    /// Loads an asset asynchronously using a background thread.
18    ///
19    /// Returns a handle immediately in the `Loading` state. The actual file I/O
20    /// and parsing happen on a rayon thread pool. Call [`process_loads`] each
21    /// frame to drain completed results and transition handles to `Loaded` or
22    /// `Failed`.
23    ///
24    /// # Deduplication
25    ///
26    /// If an asset with the same path is already loaded or loading, the existing
27    /// handle is returned without spawning a new background task.
28    ///
29    /// # Arguments
30    ///
31    /// * `path` - Path to the asset file (relative to asset root)
32    ///
33    /// # Example
34    ///
35    /// ```ignore
36    /// let handle = server.load_async::<MyAsset>("data/config.test");
37    /// // ... later, in the game loop:
38    /// server.process_loads();
39    /// if server.is_loaded(&handle) {
40    ///     let asset = server.get(&handle).unwrap();
41    /// }
42    /// ```
43    pub fn load_async<A: Asset>(&mut self, path: impl AsRef<Path>) -> AssetHandle<A> {
44        let asset_path = AssetPath::new(path.as_ref().to_str().unwrap_or_default());
45
46        // Deduplication: return existing handle if path already loaded/loading
47        if let Some(handle) = self.storage.get_handle_by_path::<A>(asset_path.as_str()) {
48            return handle;
49        }
50
51        // Reserve handle, set to Loading
52        let handle = self.storage.reserve_with_path::<A>(asset_path.clone());
53        if let Some(entry) = self.storage.get_entry_mut::<A>(&handle) {
54            entry.set_progress(0.0);
55        }
56
57        // Look up loader by extension
58        let extension = match asset_path.extension() {
59            Some(ext) => ext.to_string(),
60            None => {
61                if let Some(entry) = self.storage.get_entry_mut::<A>(&handle) {
62                    entry.set_failed("No file extension");
63                }
64                return handle;
65            }
66        };
67        let loader = match self.loaders.get(&extension) {
68            Some(l) => l.clone_boxed(),
69            None => {
70                if let Some(entry) = self.storage.get_entry_mut::<A>(&handle) {
71                    entry.set_failed(format!("No loader for extension: {}", extension));
72                }
73                return handle;
74            }
75        };
76
77        // Capture values for background thread
78        let asset_root = self.asset_root.clone();
79        let sender = self.load_sender.clone();
80        let handle_index = handle.index();
81        let handle_generation = handle.generation();
82        let asset_id = AssetId::of::<A>();
83        let path_str = asset_path.as_str().to_string();
84
85        rayon::spawn(move || {
86            let full_path = asset_root.join(&path_str);
87            let result = std::fs::read(&full_path)
88                .map_err(|e| AssetLoadError::io_error(&full_path, e))
89                .and_then(|bytes| {
90                    let owned_path = AssetPath::from_string(path_str);
91                    let mut context = LoadContext::new(owned_path);
92                    loader.load_erased(&bytes, &mut context)
93                });
94            let _ = sender.send(LoadResult {
95                handle_index,
96                handle_generation,
97                asset_id,
98                result,
99            });
100        });
101
102        handle
103    }
104}
105
106#[cfg(feature = "native")]
107impl super::core::AssetServer {
108    /// Drains completed async load results and applies them to asset storage.
109    ///
110    /// This must be called from the main thread (typically once per frame) to
111    /// transition async-loaded assets from `Loading` to `Loaded` or `Failed`.
112    ///
113    /// # Returns
114    ///
115    /// The number of load results processed in this call.
116    pub fn process_loads(&mut self) -> usize {
117        let mut count = 0;
118        while let Ok(load_result) = self.load_receiver.try_recv() {
119            match load_result.result {
120                Ok(boxed) => {
121                    self.storage.set_loaded_raw(
122                        load_result.asset_id,
123                        load_result.handle_index,
124                        load_result.handle_generation,
125                        boxed,
126                    );
127                }
128                Err(error) => {
129                    self.storage.set_failed_raw(
130                        load_result.asset_id,
131                        load_result.handle_index,
132                        load_result.handle_generation,
133                        error.to_string(),
134                    );
135                }
136            }
137            count += 1;
138        }
139        count
140    }
141}