1use crate::asset::{decode_obj_vertices_for_async, decode_rgba_image_file};
6use std::collections::{HashMap, HashSet};
7use std::sync::mpsc::{self, Receiver, Sender, SyncSender};
8use std::sync::{Arc, Mutex};
9use std::thread;
10
11#[derive(Debug)]
12pub struct TextureReloadCompletion {
13 pub cache_key: String,
14 pub rgba: Vec<u8>,
15 pub width: u32,
16 pub height: u32,
17 pub entity_ids: Vec<usize>,
18}
19
20#[derive(Debug)]
21pub struct ObjLoadCompletion {
22 pub path: String,
23 pub vertices: Vec<crate::gpu_types::Vertex>,
24 pub aabb: gizmo_math::Aabb,
25 pub handle_ids: Vec<usize>,
26}
27
28#[derive(Debug)]
30pub struct GltfImportCompletion {
31 pub path: String,
32 pub document: gltf::Document,
33 pub buffers: Vec<gltf::buffer::Data>,
34 pub images: Vec<gltf::image::Data>,
35}
36
37#[derive(Debug)]
38pub struct GltfImportError {
39 pub path: String,
40 pub message: String,
41}
42
43#[derive(Debug)]
44pub struct CompletedAsyncLoads {
45 pub textures: Vec<TextureReloadCompletion>,
46 pub objs: Vec<ObjLoadCompletion>,
47 pub gltfs: Vec<GltfImportCompletion>,
48 pub gltf_errors: Vec<GltfImportError>,
49}
50
51enum Job {
52 Texture { request_path: String },
53 Obj { path: String },
54 Gltf { path: String },
55}
56
57enum WorkerMsg {
58 Texture {
59 request_path: String,
60 cache_key: String,
61 result: Result<(Vec<u8>, u32, u32), String>,
62 },
63 Obj {
64 path: String,
65 result: Result<(Vec<crate::gpu_types::Vertex>, gizmo_math::Aabb), String>,
66 },
67 Gltf {
68 path: String,
69 result: Result<
70 (
71 gltf::Document,
72 Vec<gltf::buffer::Data>,
73 Vec<gltf::image::Data>,
74 ),
75 String,
76 >,
77 },
78}
79
80struct LoaderShared {
81 job_tx: SyncSender<Job>,
82 result_rx: Receiver<WorkerMsg>,
83 texture_waiters: HashMap<String, Vec<usize>>,
85 obj_waiters: HashMap<String, Vec<usize>>,
86 texture_inflight: HashSet<String>,
87 obj_inflight: HashSet<String>,
88 gltf_inflight: HashSet<String>,
89}
90
91pub struct AsyncAssetLoader {
93 shared: Arc<Mutex<LoaderShared>>,
94 _worker: Option<thread::JoinHandle<()>>,
95 #[allow(dead_code)]
96 result_tx: Sender<WorkerMsg>,
97}
98
99impl AsyncAssetLoader {
100 pub fn new() -> Self {
101 let (job_tx, job_rx) = mpsc::sync_channel::<Job>(64);
102 let (result_tx, result_rx) = mpsc::channel::<WorkerMsg>();
103
104 let worker_job_rx = job_rx;
105 let worker_result_tx = result_tx.clone();
106 #[cfg(not(target_arch = "wasm32"))]
107 let _worker = Some(
108 thread::Builder::new()
109 .name("gizmo-async-assets".into())
110 .spawn(move || {
111 for job in worker_job_rx {
112 match job {
113 Job::Texture { request_path } => {
114 let cache_key = std::path::Path::new(&request_path)
115 .canonicalize()
116 .map(|p| p.to_string_lossy().into_owned())
117 .unwrap_or_else(|_| request_path.clone());
118 let result = decode_rgba_image_file(&request_path);
119 let _ = worker_result_tx.send(WorkerMsg::Texture {
120 request_path,
121 cache_key,
122 result,
123 });
124 }
125 Job::Obj { path } => {
126 let result = decode_obj_vertices_for_async(&path);
127 let _ = worker_result_tx.send(WorkerMsg::Obj { path, result });
128 }
129 Job::Gltf { path } => {
130 let result = gltf::import(&path).map_err(|e| e.to_string());
131 let _ = worker_result_tx.send(WorkerMsg::Gltf { path, result });
132 }
133 }
134 }
135 })
136 .expect("spawn async asset worker"),
137 );
138
139 #[cfg(target_arch = "wasm32")]
140 let _worker = None;
141
142 Self {
143 shared: Arc::new(Mutex::new(LoaderShared {
144 job_tx,
145 result_rx,
146 texture_waiters: HashMap::new(),
147 obj_waiters: HashMap::new(),
148 texture_inflight: HashSet::new(),
149 obj_inflight: HashSet::new(),
150 gltf_inflight: HashSet::new(),
151 })),
152 _worker,
153 result_tx,
154 }
155 }
156
157 pub fn request_texture_reload(&self, request_path: String, handle_id: usize) {
160 let mut g = self.shared.lock().expect("async asset mutex");
161 g.texture_waiters
162 .entry(request_path.clone())
163 .or_default()
164 .push(handle_id);
165 if g.texture_inflight.insert(request_path.clone()) {
166 #[cfg(not(target_arch = "wasm32"))]
167 {
168 let _ = g.job_tx.send(Job::Texture { request_path });
169 }
170 #[cfg(target_arch = "wasm32")]
171 {
172 let result_tx = self.result_tx.clone();
173 let path = request_path.clone();
174 wasm_bindgen_futures::spawn_local(async move {
175 let result = fetch_and_decode_texture_wasm(&path).await;
176 let cache_key = path.clone();
177 let _ = result_tx.send(WorkerMsg::Texture {
178 request_path: path,
179 cache_key,
180 result,
181 });
182 });
183 }
184 }
185 }
186
187 pub fn request_obj_load(&self, path: String, handle_id: usize) {
189 let mut g = self.shared.lock().expect("async asset mutex");
190 g.obj_waiters
191 .entry(path.clone())
192 .or_default()
193 .push(handle_id);
194 if g.obj_inflight.insert(path.clone()) {
195 #[cfg(not(target_arch = "wasm32"))]
196 {
197 let _ = g.job_tx.send(Job::Obj { path });
198 }
199 #[cfg(target_arch = "wasm32")]
200 {
201 let result_tx = self.result_tx.clone();
202 let path_clone = path.clone();
203 wasm_bindgen_futures::spawn_local(async move {
204 let result = fetch_and_decode_obj_wasm(&path_clone).await;
205 let _ = result_tx.send(WorkerMsg::Obj {
206 path: path_clone,
207 result,
208 });
209 });
210 }
211 }
212 }
213
214 pub fn request_gltf_import(&self, path: String) -> bool {
216 tracing::info!(">>> request_gltf_import çağrıldı: {}", path);
217 let mut g = self.shared.lock().expect("async asset mutex");
218 if g.gltf_inflight.contains(&path) {
219 tracing::info!(">>> request_gltf_import: Model zaten yükleniyor!");
220 return false;
221 }
222 g.gltf_inflight.insert(path.clone());
223
224 #[cfg(not(target_arch = "wasm32"))]
225 {
226 let ok = g.job_tx.send(Job::Gltf { path }).is_ok();
227 tracing::info!(">>> request_gltf_import: İşlem gönderildi mi? {}", ok);
228 ok
229 }
230 #[cfg(target_arch = "wasm32")]
231 {
232 let result_tx = self.result_tx.clone();
233 let path_clone = path.clone();
234 wasm_bindgen_futures::spawn_local(async move {
235 let result = fetch_and_parse_gltf_wasm(&path_clone).await;
236 let _ = result_tx.send(WorkerMsg::Gltf {
237 path: path_clone,
238 result,
239 });
240 });
241 true
242 }
243 }
244
245 pub fn drain_completed(&self) -> CompletedAsyncLoads {
247 let mut out = CompletedAsyncLoads {
248 textures: Vec::new(),
249 objs: Vec::new(),
250 gltfs: Vec::new(),
251 gltf_errors: Vec::new(),
252 };
253
254 let mut g = self.shared.lock().expect("async asset mutex");
255 while let Ok(msg) = g.result_rx.try_recv() {
256 match msg {
257 WorkerMsg::Texture {
258 request_path,
259 cache_key,
260 result,
261 } => {
262 g.texture_inflight.remove(&request_path);
263 let entity_ids = g.texture_waiters.remove(&request_path).unwrap_or_default();
264 if entity_ids.is_empty() {
265 continue;
266 }
267 match result {
268 Ok((rgba, width, height)) => {
269 out.textures.push(TextureReloadCompletion {
270 cache_key,
271 rgba,
272 width,
273 height,
274 entity_ids,
275 });
276 }
277 Err(e) => {
278 tracing::error!(
279 "[AsyncAssetLoader] Texture decode failed ({request_path}): {e}"
280 );
281 }
282 }
283 }
284 WorkerMsg::Obj { path, result } => {
285 g.obj_inflight.remove(&path);
286 let handle_ids = g.obj_waiters.remove(&path).unwrap_or_default();
287 if handle_ids.is_empty() {
288 continue;
289 }
290 match result {
291 Ok((vertices, aabb)) => {
292 out.objs.push(ObjLoadCompletion {
293 path,
294 vertices,
295 aabb,
296 handle_ids,
297 });
298 }
299 Err(e) => {
300 tracing::error!("[AsyncAssetLoader] OBJ decode failed ({path}): {e}");
301 }
302 }
303 }
304 WorkerMsg::Gltf { path, result } => {
305 g.gltf_inflight.remove(&path);
306 match result {
307 Ok((document, buffers, images)) => {
308 out.gltfs.push(GltfImportCompletion {
309 path,
310 document,
311 buffers,
312 images,
313 });
314 }
315 Err(message) => {
316 out.gltf_errors.push(GltfImportError { path, message });
317 }
318 }
319 }
320 }
321 }
322
323 out
324 }
325}
326
327impl Default for AsyncAssetLoader {
328 fn default() -> Self {
329 Self::new()
330 }
331}
332
333#[cfg(target_arch = "wasm32")]
336async fn native_fetch_bytes(url: &str) -> Result<Vec<u8>, String> {
337 use wasm_bindgen::JsCast;
338 use wasm_bindgen_futures::JsFuture;
339
340 let window = web_sys::window().ok_or("No global window found")?;
341 let resp_value = JsFuture::from(window.fetch_with_str(url))
342 .await
343 .map_err(|e| format!("Fetch failed for {url}: {e:?}"))?;
344 let resp: web_sys::Response = resp_value.dyn_into().map_err(|_| "Failed to cast to Response")?;
345
346 if !resp.ok() {
347 return Err(format!("HTTP error status for {url}: {}", resp.status()));
348 }
349
350 let array_buffer_value = JsFuture::from(resp.array_buffer().map_err(|e| format!("Failed to get array buffer: {e:?}"))?)
351 .await
352 .map_err(|e| format!("Failed to resolve array buffer: {e:?}"))?;
353 let array_buffer = js_sys::ArrayBuffer::from(array_buffer_value);
354 let uint8_array = js_sys::Uint8Array::new(&array_buffer);
355 let mut bytes = vec![0; uint8_array.length() as usize];
356 uint8_array.copy_to(&mut bytes);
357 Ok(bytes)
358}
359
360#[cfg(target_arch = "wasm32")]
361async fn fetch_and_decode_texture_wasm(path: &str) -> Result<(Vec<u8>, u32, u32), String> {
362 let bytes = native_fetch_bytes(path).await?;
363
364 let img = image::load_from_memory(&bytes)
365 .map_err(|e| format!("Cannot read texture ({path}) from memory: {e}"))?
366 .to_rgba8();
367 let (w, h) = img.dimensions();
368 Ok((img.into_raw(), w, h))
369}
370
371#[cfg(target_arch = "wasm32")]
372async fn fetch_and_parse_gltf_wasm(
373 path: &str,
374) -> Result<
375 (
376 gltf::Document,
377 Vec<gltf::buffer::Data>,
378 Vec<gltf::image::Data>,
379 ),
380 String,
381> {
382 let bytes = native_fetch_bytes(path).await?;
383
384 gltf::import_slice(&bytes)
385 .map_err(|e| format!("glTF parse slice failed ({path}): {e}"))
386}
387
388#[cfg(target_arch = "wasm32")]
389async fn fetch_and_decode_obj_wasm(
390 path: &str,
391) -> Result<(Vec<crate::gpu_types::Vertex>, gizmo_math::Aabb), String> {
392 let bytes = native_fetch_bytes(path).await?;
393
394 let mut reader = std::io::Cursor::new(bytes);
395 let (models, _) = tobj::load_obj_buf(
396 &mut reader,
397 &tobj::LoadOptions {
398 single_index: true,
399 triangulate: true,
400 ignore_points: true,
401 ignore_lines: true,
402 },
403 |_| Err(tobj::LoadError::OpenFileFailed),
404 )
405 .map_err(|e| format!("OBJ load from buffer failed ({path}): {e}"))?;
406
407 if models.is_empty() {
408 return Err(format!("OBJ file contains no models: {path}"));
409 }
410
411 let mut aabb = gizmo_math::Aabb::empty();
412 let mut vertices = Vec::new();
413
414 for model in &models {
415 let m = &model.mesh;
416 let has_normals = !m.normals.is_empty();
417 let has_texcoords = !m.texcoords.is_empty();
418
419 for &raw_idx in &m.indices {
420 let idx = raw_idx as usize;
421 let pos_base = idx * 3;
422 if pos_base + 2 >= m.positions.len() {
423 return Err(format!("OBJ ({path}): position index out of range"));
424 }
425 let position = [
426 m.positions[pos_base],
427 m.positions[pos_base + 1],
428 m.positions[pos_base + 2],
429 ];
430 aabb.extend(gizmo_math::Vec3::new(position[0], position[1], position[2]));
431
432 let normal = if has_normals {
433 let n_base = idx * 3;
434 [
435 m.normals[n_base],
436 m.normals[n_base + 1],
437 m.normals[n_base + 2],
438 ]
439 } else {
440 [0.0, 1.0, 0.0]
441 };
442
443 let tex_coords = if has_texcoords {
444 let uv_base = idx * 2;
445 [m.texcoords[uv_base], 1.0 - m.texcoords[uv_base + 1]]
446 } else {
447 [0.0, 0.0]
448 };
449
450 vertices.push(crate::renderer::Vertex {
451 position,
452 normal,
453 tex_coords,
454 color: [1.0, 1.0, 1.0],
455 joint_indices: [0; 4],
456 joint_weights: [0.0; 4],
457 });
458 }
459 }
460
461 Ok((vertices, aabb))
462}