From 6119c7dee4249f1d98710368d06c6cc18f6cb04c Mon Sep 17 00:00:00 2001 From: Dzmitry Malyshau Date: Sun, 5 Nov 2023 08:36:28 -0800 Subject: [PATCH] Embedded gltf textures Also tweaks blade-asset API to borrow choir's ExecutionContext. --- Cargo.toml | 1 + blade-asset/src/arena.rs | 24 +++- blade-asset/src/flat.rs | 1 + blade-asset/src/lib.rs | 78 ++++++++--- blade-asset/tests/main.rs | 4 +- blade-graphics/Cargo.toml | 2 +- blade-render/Cargo.toml | 3 +- blade-render/code/fill-gbuf.wgsl | 2 +- blade-render/src/model/mod.rs | 223 +++++++++++++++++++++---------- blade-render/src/shader.rs | 4 +- blade-render/src/texture/mod.rs | 8 +- examples/scene/main.rs | 43 +++--- 12 files changed, 273 insertions(+), 120 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bfc4811c..5b23d1f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ log = "0.4" mint = "0.5" naga = { version = "0.14", features = ["wgsl-in", "span", "validate"] } profiling = "1" +slab = "0.4" strum = { version = "0.25", features = ["derive"] } web-sys = "0.3.60" diff --git a/blade-asset/src/arena.rs b/blade-asset/src/arena.rs index 2bf9bf78..7cb77d8e 100644 --- a/blade-asset/src/arena.rs +++ b/blade-asset/src/arena.rs @@ -124,14 +124,14 @@ impl Arena { unsafe { first_ptr.add(handle.0.index as usize) } } - pub fn dealloc(&self, handle: Handle) -> T { + pub fn _dealloc(&self, handle: Handle) -> T { let mut freeman = self.freeman.lock().unwrap(); freeman.free_list.push(handle.0); let ptr = self.get_mut_ptr(handle); - unsafe { mem::take(&mut *ptr) } + mem::take(unsafe { &mut *ptr }) } - pub fn for_each(&self, mut fun: impl FnMut(Handle, &T)) { + fn for_internal(&self, mut fun: impl FnMut(Address, *mut T)) { let mut freeman = self.freeman.lock().unwrap(); freeman.free_list.sort(); // enables fast search for (chunk_index, chunk_start) in self.chunks[..freeman.chunk_bases.len()] @@ -147,14 +147,26 @@ impl Arena { chunk, }; if freeman.free_list.binary_search(&address).is_err() { - //Note: this is only safe if `get_mut_ptr` isn't called + //Note: accessing this is only safe if `get_mut_ptr` isn't called // for example, during hot reloading. - let item = unsafe { &*first_ptr.add(index) }; - fun(Handle(address, PhantomData), item); + fun(address, unsafe { first_ptr.add(index) }); } } } } + + pub fn for_each(&self, mut fun: impl FnMut(Handle, &T)) { + self.for_internal(|address, ptr| fun(Handle(address, PhantomData), unsafe { &*ptr })) + } + + pub fn dealloc_each(&self, mut fun: impl FnMut(Handle, T)) { + self.for_internal(|address, ptr| { + fun( + Handle(address, PhantomData), + mem::take(unsafe { &mut *ptr }), + ) + }) + } } impl Drop for FreeManager { diff --git a/blade-asset/src/flat.rs b/blade-asset/src/flat.rs index 4119347b..775f2cd4 100644 --- a/blade-asset/src/flat.rs +++ b/blade-asset/src/flat.rs @@ -42,6 +42,7 @@ macro_rules! impl_basic { impl_basic!(bool); impl_basic!(u32); impl_basic!(u64); +impl_basic!(usize); impl_basic!(f32); /* diff --git a/blade-asset/src/lib.rs b/blade-asset/src/lib.rs index 63a0e131..6e8a18db 100644 --- a/blade-asset/src/lib.rs +++ b/blade-asset/src/lib.rs @@ -94,6 +94,7 @@ impl Default for Slot { } } +#[derive(Default)] struct Inner { result: Vec, dependencies: Vec, @@ -143,6 +144,21 @@ impl Cooker { } } + /// Create a new container with no data, no path, and no hasher. + pub fn new_embedded() -> Self { + Self { + inner: Mutex::new(Inner::default()), + base_path: Default::default(), + _phantom: PhantomData, + } + } + + pub fn extract_embedded(&self) -> Vec { + let mut inner = self.inner.lock().unwrap(); + assert!(inner.dependencies.is_empty()); + mem::take(&mut inner.result) + } + /// Return the base path of the asset. pub fn base_path(&self) -> &Path { &self.base_path @@ -197,12 +213,12 @@ pub trait Baker: Sized + Send + Sync + 'static { extension: &str, meta: Self::Meta, cooker: Arc>, - exe_context: choir::ExecutionContext, + exe_context: &choir::ExecutionContext, ); /// Produce the output bsed on a cooked asset. /// /// This method is also called within a task `exe_context`. - fn serve(&self, cooked: Self::Data<'_>, exe_context: choir::ExecutionContext) -> Self::Output; + fn serve(&self, cooked: Self::Data<'_>, exe_context: &choir::ExecutionContext) -> Self::Output; /// Delete the output of an asset. fn delete(&self, output: Self::Output); } @@ -318,8 +334,8 @@ impl AssetManager { } } - pub fn get_main_source_path(&self, handle: Handle) -> &Path { - self.slots[handle.inner].sources.first().unwrap() + pub fn get_main_source_path(&self, handle: Handle) -> Option<&PathBuf> { + self.slots[handle.inner].sources.first() } fn make_target_path(&self, base_path: &Path, file_name: &Path, meta: &B::Meta) -> PathBuf { @@ -405,7 +421,7 @@ impl AssetManager { baker.delete(data); } let cooked = unsafe { as Flat>::read(inner.result.as_ptr()) }; - let target = baker.serve(cooked, exe_context); + let target = baker.serve(cooked, &exe_context); unsafe { *dr.data = Some(target); *dr.version = version; @@ -427,7 +443,7 @@ impl AssetManager { Some(data) => data, None => cooker_arg.add_dependency(&file_name), }; - baker.cook(&source, extension, meta, cooker_arg, exe_context); + baker.cook(&source, extension, meta, cooker_arg, &exe_context); }); load_task.depend_on(&cook_task); @@ -447,7 +463,7 @@ impl AssetManager { let mut data = Vec::new(); file.read_to_end(&mut data).unwrap(); let cooked = unsafe { as Flat>::read(data.as_ptr()) }; - let target = baker.serve(cooked, exe_context); + let target = baker.serve(cooked, &exe_context); let dr = data_ref; unsafe { *dr.data = Some(target); @@ -467,11 +483,14 @@ impl AssetManager { let (handle, slot_ptr) = self.slots.alloc_default(); let slot = unsafe { &mut *slot_ptr }; assert_eq!(slot.version, 0); - slot.base_path = source_path - .parent() - .unwrap_or_else(|| Path::new(".")) - .to_owned(); - slot.meta = Box::into_raw(Box::new(meta)) as *const _; + *slot = Slot { + base_path: source_path + .parent() + .unwrap_or_else(|| Path::new(".")) + .to_owned(), + meta: Box::into_raw(Box::new(meta)) as *const _, + ..Default::default() + }; let file_name = Path::new(source_path.file_name().unwrap()); let (version, _) = self.create_impl(slot, file_name, None).unwrap(); @@ -495,8 +514,10 @@ impl AssetManager { let (handle, slot_ptr) = self.slots.alloc_default(); let slot = unsafe { &mut *slot_ptr }; assert_eq!(slot.version, 0); - slot.base_path = Default::default(); - slot.meta = Box::into_raw(Box::new(meta)) as *const _; + *slot = Slot { + meta: Box::into_raw(Box::new(meta)) as *const _, + ..Default::default() + }; let (version, _) = self.create_impl(slot, name, Some(data)).unwrap(); @@ -533,12 +554,35 @@ impl AssetManager { (handle, task) } + /// Load an asset that has been pre-cooked already. + /// + /// Expected to run inside a task. + pub fn load_cooked_inside_task( + &self, + cooked: B::Data<'_>, + exe_context: &choir::ExecutionContext, + ) -> Handle { + let value = self.baker.serve(cooked, exe_context); + let (handle, slot_ptr) = self.slots.alloc_default(); + let slot = unsafe { &mut *slot_ptr }; + assert_eq!(slot.version, 0); + *slot = Slot { + version: 1, + data: Some(value), + ..Slot::default() + }; + Handle { + inner: handle, + version: slot.version, + } + } + /// Clear the asset manager by deleting all the stored assets. /// /// Invalidates all handles produced from loading assets. pub fn clear(&self) { - for (_key, handle) in self.paths.lock().unwrap().drain() { - let slot = self.slots.dealloc(handle.inner); + self.paths.lock().unwrap().clear(); + self.slots.dealloc_each(|_handle, slot| { if let Some(task) = slot.load_task { task.join(); } @@ -550,7 +594,7 @@ impl AssetManager { let _ = Box::from_raw(slot.meta as *mut B::Meta); } } - } + }) } /// Hot reload a changed asset. diff --git a/blade-asset/tests/main.rs b/blade-asset/tests/main.rs index 477dfd21..37f48dce 100644 --- a/blade-asset/tests/main.rs +++ b/blade-asset/tests/main.rs @@ -20,13 +20,13 @@ impl blade_asset::Baker for Baker { _extension: &str, meta: u32, cooker: Arc>, - _exe_context: choir::ExecutionContext, + _exe_context: &choir::ExecutionContext, ) { assert!(self.allow_cooking.load(Ordering::SeqCst)); let _ = cooker.add_dependency("README.md".as_ref()); cooker.finish(meta); } - fn serve(&self, cooked: u32, _exe_context: choir::ExecutionContext) -> usize { + fn serve(&self, cooked: u32, _exe_context: &choir::ExecutionContext) -> usize { cooked as usize } fn delete(&self, _output: usize) {} diff --git a/blade-graphics/Cargo.toml b/blade-graphics/Cargo.toml index 45a1859a..cb9739b6 100644 --- a/blade-graphics/Cargo.toml +++ b/blade-graphics/Cargo.toml @@ -32,7 +32,7 @@ ash-window = "0.12" gpu-alloc = "0.6" gpu-alloc-ash = "0.6" naga = { workspace = true, features = ["spv-out"] } -slab = "0.4" +slab = { workspace = true } [target.'cfg(any(gles, target_arch = "wasm32"))'.dependencies] # Version contains `glGetProgramResource` diff --git a/blade-render/Cargo.toml b/blade-render/Cargo.toml index 44cf9909..8442c16e 100644 --- a/blade-render/Cargo.toml +++ b/blade-render/Cargo.toml @@ -11,7 +11,7 @@ repository = "https://github.com/kvark/blade" [features] default = ["asset"] -asset = ["gltf" , "base64", "exr", "mikktspace", "texpresso", "zune-core", "zune-jpeg", "zune-png", "zune-imageprocs"] +asset = ["gltf" , "base64", "exr", "mikktspace", "slab", "texpresso", "zune-core", "zune-jpeg", "zune-png", "zune-imageprocs"] [dependencies] base64 = { workspace = true, optional = true } @@ -28,6 +28,7 @@ log = { workspace = true } mikktspace = { package = "bevy_mikktspace", version = "0.10", optional = true } mint = { workspace = true } profiling = { workspace = true } +slab = { workspace = true, optional = true } strum = { workspace = true } texpresso = { version = "2.0", optional = true } #zune-core = { version = "0.2", optional = true } diff --git a/blade-render/code/fill-gbuf.wgsl b/blade-render/code/fill-gbuf.wgsl index 4da6baa0..fd08b891 100644 --- a/blade-render/code/fill-gbuf.wgsl +++ b/blade-render/code/fill-gbuf.wgsl @@ -135,7 +135,7 @@ fn main(@builtin(global_invocation_id) global_id: vec3) { if (enable_debug && (debug.draw_flags & DebugDrawFlags_SPACE) != 0u) { let normal_len = 0.15 * intersection.t; let side = 0.05 * intersection.t; - debug_line(hit_position, hit_position + normal_len * normal_geo, 0xFFFFFFu); + debug_line(hit_position, hit_position + normal_len * qrot(geo_to_world_rot, normal_geo), 0xFFFFFFu); debug_line(hit_position - side * tangent_geo, hit_position + side * tangent_geo, 0x808080u); debug_line(hit_position - side * bitangent_geo, hit_position + side * bitangent_geo, 0x808080u); } diff --git a/blade-render/src/model/mod.rs b/blade-render/src/model/mod.rs index 92cd7db5..5f73a9ea 100644 --- a/blade-render/src/model/mod.rs +++ b/blade-render/src/model/mod.rs @@ -58,11 +58,19 @@ pub struct Model { pub acceleration_structure: blade_graphics::AccelerationStructure, } +#[derive(blade_macros::Flat, Default)] +struct TextureReference<'a> { + path: Cow<'a, [u8]>, + embedded_data: Cow<'a, [u8]>, + //Note: this isn't used for anything during deserialization + source_index: usize, +} + #[derive(blade_macros::Flat)] struct CookedMaterial<'a> { - base_color_path: Cow<'a, [u8]>, + base_color: TextureReference<'a>, base_color_factor: [f32; 4], - normal_path: Cow<'a, [u8]>, + normal: TextureReference<'a>, transparent: bool, } @@ -295,6 +303,29 @@ struct PendingOperations { blas_constructs: Vec, } +enum TextureSource { + Path(String), + Embedded( + Option, + Arc>, + ), +} + +#[cfg(feature = "asset")] +impl TextureReference<'_> { + fn complete(&mut self, sources: &slab::Slab) { + match sources.get(self.source_index) { + Some(&TextureSource::Embedded(ref _task, ref sub_cooker)) => { + self.embedded_data = Cow::Owned(sub_cooker.extract_embedded()); + } + Some(&TextureSource::Path(ref relative)) => { + self.path = Cow::Owned(relative.as_bytes().to_owned()); + } + None => {} + } + } +} + pub struct Baker { gpu_context: Arc, pending_operations: Mutex, @@ -339,6 +370,81 @@ impl Baker { } } } + + #[cfg(feature = "asset")] + fn cook_texture( + &self, + texture: gltf::texture::Texture, + meta: super::texture::Meta, + data_buffers: &[Vec], + ) -> TextureSource { + match texture.source().source() { + gltf::image::Source::View { view, mime_type } => { + let sub_cooker = Arc::new(blade_asset::Cooker::new_embedded()); + let cooker = Arc::clone(&sub_cooker); + let baker = Arc::clone(&self.asset_textures.baker); + let buffer = &data_buffers[view.buffer().index()]; + let data = buffer[view.offset()..view.offset() + view.length()].to_vec(); + let extension = mime_type.split_once('/').unwrap().1.to_string(); + let task = + self.asset_textures + .choir + .spawn("embedded cook") + .init(move |exe_ontext| { + blade_asset::Baker::cook( + baker.as_ref(), + &data, + &extension, + meta, + cooker, + &exe_ontext, + ); + }); + TextureSource::Embedded(Some(task), sub_cooker) + } + gltf::image::Source::Uri { uri, mime_type: _ } => { + let relative = if let Some(_rest) = uri.strip_prefix("data:") { + panic!("Data URL isn't supported for textures yet"); + } else if let Some(rest) = uri.strip_prefix("file://") { + rest + } else if let Some(rest) = uri.strip_prefix("file:") { + rest + } else { + uri + }; + if PRELOAD_TEXTURES { + self.asset_textures.load(relative, meta); + } + TextureSource::Path(relative.to_string()) + } + } + } + + fn serve_texture( + &self, + texture_ref: &TextureReference, + meta: super::texture::Meta, + exe_context: &choir::ExecutionContext, + ) -> Option> { + if !texture_ref.path.is_empty() { + let path_str = str::from_utf8(&texture_ref.path).unwrap(); + let (handle, task) = self.asset_textures.load(path_str, meta); + exe_context.add_fork(&task); + Some(handle) + } else if !texture_ref.embedded_data.is_empty() { + let cooked = unsafe { + as blade_asset::Flat>::read( + texture_ref.embedded_data.as_ptr(), + ) + }; + Some( + self.asset_textures + .load_cooked_inside_task(cooked, exe_context), + ) + } else { + None + } + } } impl blade_asset::Baker for Baker { @@ -352,7 +458,7 @@ impl blade_asset::Baker for Baker { extension: &str, meta: Meta, cooker: Arc>, - exe_context: choir::ExecutionContext, + exe_context: &choir::ExecutionContext, ) { match extension { #[cfg(feature = "asset")] @@ -384,30 +490,8 @@ impl blade_asset::Baker for Baker { } buffers.push(data); } - let mut texture_paths = Vec::new(); - for texture in document.textures() { - let relative = match texture.source().source() { - gltf::image::Source::Uri { uri, .. } => { - if let Some(rest) = uri.strip_prefix("data:") { - let (_before, after) = rest.split_once(";base64,").unwrap(); - let _data = ENCODING_ENGINE.decode(after).unwrap(); - panic!("Data URL isn't supported here yet"); - } else if let Some(rest) = uri.strip_prefix("file://") { - rest - } else if let Some(rest) = uri.strip_prefix("file:") { - rest - } else { - uri - } - } - gltf::image::Source::View { .. } => { - panic!("Embedded images are not supported yet") - } - }; - let full = cooker.base_path().join(relative); - texture_paths.push(full.to_str().unwrap().to_string()); - } + let mut sources = slab::Slab::new(); let mut model = CookedModel { name: &[], materials: Vec::new(), @@ -416,30 +500,33 @@ impl blade_asset::Baker for Baker { for g_material in document.materials() { let pbr = g_material.pbr_metallic_roughness(); model.materials.push(CookedMaterial { - base_color_path: Cow::Owned(match pbr.base_color_texture() { - Some(info) => { - let path = &texture_paths[info.texture().index()]; - if PRELOAD_TEXTURES { - self.asset_textures.load(path, META_BASE_COLOR); - } - path.as_bytes().to_vec() - } - None => Vec::new(), - }), + base_color: TextureReference { + source_index: match pbr.base_color_texture() { + Some(info) => sources.insert(self.cook_texture( + info.texture(), + META_BASE_COLOR, + &buffers, + )), + None => !0, + }, + ..Default::default() + }, base_color_factor: pbr.base_color_factor(), - normal_path: Cow::Owned(match g_material.normal_texture() { - Some(info) => { - let path = &texture_paths[info.texture().index()]; - if PRELOAD_TEXTURES { - self.asset_textures.load(path, META_BASE_COLOR); - } - path.as_bytes().to_vec() - } - None => Vec::new(), - }), + normal: TextureReference { + source_index: match pbr.base_color_texture() { + Some(info) => sources.insert(self.cook_texture( + info.texture(), + META_NORMAL, + &buffers, + )), + None => !0, + }, + ..Default::default() + }, transparent: g_material.alpha_mode() != gltf::material::AlphaMode::Opaque, }); } + let mut flattened_geos = Vec::new(); for g_scene in document.scenes() { for g_node in g_scene.nodes() { @@ -476,39 +563,41 @@ impl blade_asset::Baker for Baker { geo.indices = Cow::Owned(indices); }, ); + + let mut dependencies = vec![gen_tangents]; + for (_, source) in sources.iter_mut() { + if let TextureSource::Embedded(ref mut task, _) = *source { + dependencies.push(task.take().unwrap()) + } + } + let mut finish = exe_context.fork("finish").init(move |_| { - let model = Arc::into_inner(model_shared).unwrap().into_inner().unwrap(); + let mut model = Arc::into_inner(model_shared).unwrap().into_inner().unwrap(); + for material in model.materials.iter_mut() { + material.base_color.complete(&sources); + material.normal.complete(&sources); + } cooker.finish(model); }); - finish.depend_on(&gen_tangents); + for dependency in dependencies { + finish.depend_on(&dependency); + } } other => panic!("Unknown model extension: {}", other), } } - fn serve(&self, model: CookedModel<'_>, exe_context: choir::ExecutionContext) -> Self::Output { + fn serve(&self, model: CookedModel<'_>, exe_context: &choir::ExecutionContext) -> Self::Output { let mut materials = Vec::with_capacity(model.materials.len()); for material in model.materials.iter() { - let base_color_texture = if material.base_color_path.is_empty() { - None - } else { - let path_str = str::from_utf8(&material.base_color_path).unwrap(); - let (handle, task) = self.asset_textures.load(path_str, META_BASE_COLOR); - exe_context.add_fork(&task); - Some(handle) - }; - let normal_texture = if material.normal_path.is_empty() { - None - } else { - let path_str = str::from_utf8(&material.normal_path).unwrap(); - let (handle, task) = self.asset_textures.load(path_str, META_NORMAL); - exe_context.add_fork(&task); - Some(handle) - }; materials.push(Material { - base_color_texture, + base_color_texture: self.serve_texture( + &material.base_color, + META_BASE_COLOR, + exe_context, + ), base_color_factor: material.base_color_factor, - normal_texture, + normal_texture: self.serve_texture(&material.normal, META_NORMAL, exe_context), transparent: material.transparent, }); } diff --git a/blade-render/src/shader.rs b/blade-render/src/shader.rs index b9b8cddb..d99c9de3 100644 --- a/blade-render/src/shader.rs +++ b/blade-render/src/shader.rs @@ -123,7 +123,7 @@ impl blade_asset::Baker for Baker { extension: &str, _meta: Meta, cooker: Arc>, - _exe_context: choir::ExecutionContext, + _exe_context: &choir::ExecutionContext, ) { assert_eq!(extension, "wgsl"); let text_out = parse_shader(source, &cooker, &self.expansions); @@ -131,7 +131,7 @@ impl blade_asset::Baker for Baker { data: text_out.as_bytes(), }); } - fn serve(&self, cooked: CookedShader, _exe_context: choir::ExecutionContext) -> Shader { + fn serve(&self, cooked: CookedShader, _exe_context: &choir::ExecutionContext) -> Shader { let source = str::from_utf8(cooked.data).unwrap(); let raw = self .gpu_context diff --git a/blade-render/src/texture/mod.rs b/blade-render/src/texture/mod.rs index 1182d176..eb63f110 100644 --- a/blade-render/src/texture/mod.rs +++ b/blade-render/src/texture/mod.rs @@ -111,7 +111,7 @@ impl blade_asset::Baker for Baker { extension: &str, meta: Meta, cooker: Arc>, - exe_context: choir::ExecutionContext, + exe_context: &choir::ExecutionContext, ) { use blade_graphics::TextureFormat as Tf; enum PlainData { @@ -340,7 +340,11 @@ impl blade_asset::Baker for Baker { } } - fn serve(&self, image: CookedImage<'_>, _exe_context: choir::ExecutionContext) -> Self::Output { + fn serve( + &self, + image: CookedImage<'_>, + _exe_context: &choir::ExecutionContext, + ) -> Self::Output { let name = str::from_utf8(image.name).unwrap(); let base_extent = blade_graphics::Extent { width: image.extent[0], diff --git a/examples/scene/main.rs b/examples/scene/main.rs index 74ea6f99..e3ec764c 100644 --- a/examples/scene/main.rs +++ b/examples/scene/main.rs @@ -510,19 +510,17 @@ impl Example { let aspect = extent.width as f32 / extent.height as f32; let projection_matrix = glam::Mat4::perspective_rh(self.camera.fov_y, aspect, 1.0, self.camera.depth); - let model_matrix = mint::ColumnMatrix4::from({ - let t = self.objects[obj_index].transform; - mint::RowMatrix4 { - x: t.x, - y: t.y, - z: t.z, - w: mint::Vector4 { - x: 0.0, - y: 0.0, - z: 0.0, - w: 1.0, - }, - } + let t0 = &mut self.objects[obj_index].transform; + let model_matrix = mint::ColumnMatrix4::from(mint::RowMatrix4 { + x: t0.x, + y: t0.y, + z: t0.z, + w: mint::Vector4 { + x: 0.0, + y: 0.0, + z: 0.0, + w: 1.0, + }, }); let gizmo = egui_gizmo::Gizmo::new("Object") .view_matrix(mint::ColumnMatrix4::from(view_matrix)) @@ -532,13 +530,16 @@ impl Example { .orientation(egui_gizmo::GizmoOrientation::Global) .snapping(true); if let Some(response) = gizmo.interact(ui) { - let tc = TransformComponents { + let t1 = TransformComponents { scale: response.scale, rotation: response.rotation, translation: response.translation, - }; - self.objects[obj_index].transform = tc.to_blade(); - self.have_objects_changed = true; + } + .to_blade(); + if *t0 != t1 { + *t0 = t1; + self.have_objects_changed = true; + } } } @@ -681,8 +682,8 @@ impl Example { .asset_hub .textures .get_main_source_path(handle) - .display() - .to_string(); + .map(|path| path.display().to_string()) + .unwrap_or_default(); ui.colored_label(egui::Color32::WHITE, name); } }); @@ -693,8 +694,8 @@ impl Example { .asset_hub .textures .get_main_source_path(handle) - .display() - .to_string(); + .map(|path| path.display().to_string()) + .unwrap_or_default(); ui.colored_label(egui::Color32::WHITE, name); } });