From 3aad72e9d01e8ede07f486c65972706181b7f15b Mon Sep 17 00:00:00 2001 From: andriyDev Date: Wed, 1 Oct 2025 23:13:16 -0700 Subject: [PATCH 01/14] Merge the processors with the default_processors into a single struct. --- crates/bevy_asset/src/processor/mod.rs | 49 ++++++++++++++++---------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/crates/bevy_asset/src/processor/mod.rs b/crates/bevy_asset/src/processor/mod.rs index b9459f04a47d2..4fd56d9b42170 100644 --- a/crates/bevy_asset/src/processor/mod.rs +++ b/crates/bevy_asset/src/processor/mod.rs @@ -112,9 +112,8 @@ pub struct AssetProcessorData { /// avoids needing to use [`block_on`](bevy_tasks::block_on) to set the factory). log_factory: Mutex>>, log: async_lock::RwLock>>, - processors: RwLock>>, - /// Default processors for file extensions - default_processors: RwLock, &'static str>>, + /// The processors that will be used to process assets. + processors: RwLock, sources: Arc, } @@ -132,6 +131,15 @@ pub(crate) struct ProcessingState { asset_infos: async_lock::RwLock, } +#[derive(Default)] +struct Processors { + /// Maps the type name of the processor to its instance. + type_name_to_processor: HashMap<&'static str, Arc>, + /// Maps the file extension of an asset to the type name of the processor we should use to + /// process it by default. + file_extension_to_default_processor: HashMap, &'static str>, +} + impl AssetProcessor { /// Creates a new [`AssetProcessor`] instance. pub fn new( @@ -712,40 +720,41 @@ impl AssetProcessor { /// Register a new asset processor. pub fn register_processor(&self, processor: P) { - let mut process_plans = self + let mut processors = self .data .processors .write() .unwrap_or_else(PoisonError::into_inner); #[cfg(feature = "trace")] let processor = InstrumentedAssetProcessor(processor); - process_plans.insert(core::any::type_name::

(), Arc::new(processor)); + processors + .type_name_to_processor + .insert(core::any::type_name::

(), Arc::new(processor)); } /// Set the default processor for the given `extension`. Make sure `P` is registered with [`AssetProcessor::register_processor`]. pub fn set_default_processor(&self, extension: &str) { - let mut default_processors = self + let mut processors = self .data - .default_processors + .processors .write() .unwrap_or_else(PoisonError::into_inner); - default_processors.insert(extension.into(), core::any::type_name::

()); + processors + .file_extension_to_default_processor + .insert(extension.into(), core::any::type_name::

()); } /// Returns the default processor for the given `extension`, if it exists. pub fn get_default_processor(&self, extension: &str) -> Option> { - let default_processors = self + let processors = self .data - .default_processors - .read() - .unwrap_or_else(PoisonError::into_inner); - let key = default_processors.get(extension)?; - self.data .processors .read() - .unwrap_or_else(PoisonError::into_inner) - .get(key) - .cloned() + .unwrap_or_else(PoisonError::into_inner); + let key = processors + .file_extension_to_default_processor + .get(extension)?; + processors.type_name_to_processor.get(key).cloned() } /// Returns the processor with the given `processor_type_name`, if it exists. @@ -755,7 +764,10 @@ impl AssetProcessor { .processors .read() .unwrap_or_else(PoisonError::into_inner); - processors.get(processor_type_name).cloned() + processors + .type_name_to_processor + .get(processor_type_name) + .cloned() } /// Populates the initial view of each asset by scanning the unprocessed and processed asset folders. @@ -1257,7 +1269,6 @@ impl AssetProcessorData { log_factory: Mutex::new(Some(Box::new(FileTransactionLogFactory::default()))), log: Default::default(), processors: Default::default(), - default_processors: Default::default(), } } From 2dcdf1993cf0fb88253bdfdc690b909f66c204aa Mon Sep 17 00:00:00 2001 From: andriyDev Date: Wed, 1 Oct 2025 22:55:06 -0700 Subject: [PATCH 02/14] Use the TypePath trait for registering loaders and processors. --- crates/bevy_animation/src/graph.rs | 4 +-- crates/bevy_asset/src/lib.rs | 9 ++++++- crates/bevy_asset/src/loader.rs | 3 ++- crates/bevy_asset/src/processor/mod.rs | 20 ++++++++------- crates/bevy_asset/src/processor/process.rs | 4 ++- crates/bevy_asset/src/processor/tests.rs | 10 +++++++- crates/bevy_asset/src/saver.rs | 3 ++- crates/bevy_asset/src/server/loaders.rs | 25 +++++++++++-------- crates/bevy_asset/src/transformer.rs | 4 ++- crates/bevy_audio/src/audio_source.rs | 2 +- crates/bevy_gltf/src/loader/mod.rs | 2 ++ .../bevy_image/src/compressed_image_saver.rs | 4 ++- crates/bevy_image/src/exr_texture_loader.rs | 5 ++-- crates/bevy_image/src/hdr_texture_loader.rs | 3 ++- crates/bevy_image/src/image_loader.rs | 3 ++- crates/bevy_pbr/src/meshlet/asset.rs | 2 ++ crates/bevy_scene/src/scene_loader.rs | 4 +-- crates/bevy_shader/src/shader.rs | 2 +- crates/bevy_text/src/font_loader.rs | 3 ++- examples/asset/asset_decompression.rs | 2 +- examples/asset/custom_asset.rs | 4 +-- examples/asset/processing/asset_processing.rs | 7 +++--- 22 files changed, 82 insertions(+), 43 deletions(-) diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs index e101e11209a2b..5c06783b50bc3 100644 --- a/crates/bevy_animation/src/graph.rs +++ b/crates/bevy_animation/src/graph.rs @@ -19,7 +19,7 @@ use bevy_ecs::{ system::{Res, ResMut}, }; use bevy_platform::collections::HashMap; -use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_reflect::{prelude::ReflectDefault, Reflect, TypePath}; use derive_more::derive::From; use petgraph::{ graph::{DiGraph, NodeIndex}, @@ -238,7 +238,7 @@ pub enum AnimationNodeType { /// /// The canonical extension for [`AnimationGraph`]s is `.animgraph.ron`. Plain /// `.animgraph` is supported as well. -#[derive(Default)] +#[derive(Default, TypePath)] pub struct AnimationGraphAssetLoader; /// Errors that can occur when serializing animation graphs to RON. diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index cdaf34bffe8c2..6f09e087967e3 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -769,7 +769,7 @@ mod tests { pub sub_texts: Vec, } - #[derive(Default)] + #[derive(Default, TypePath)] pub struct CoolTextLoader; #[derive(Error, Debug)] @@ -1934,6 +1934,7 @@ mod tests { .init_asset::() .register_asset_loader(CoolTextLoader); + #[derive(TypePath)] struct NestedLoadOfSubassetLoader; impl AssetLoader for NestedLoadOfSubassetLoader { @@ -2153,6 +2154,7 @@ mod tests { // Note: we can't just use the GatedReader, since currently we hold the handle until after // we've selected the reader. The GatedReader blocks this process, so we need to wait until // we gate in the loader instead. + #[derive(TypePath)] struct GatedLoader { in_loader_sender: Sender<()>, gate_receiver: Receiver<()>, @@ -2456,6 +2458,7 @@ mod tests { #[derive(Serialize, Deserialize, Default)] struct U8LoaderSettings(u8); + #[derive(TypePath)] struct U8Loader; impl AssetLoader for U8Loader { @@ -2534,6 +2537,7 @@ mod tests { let (mut app, dir) = create_app(); dir.insert_asset(Path::new("test.txt"), &[]); + #[derive(TypePath)] struct TwoSubassetLoader; impl AssetLoader for TwoSubassetLoader { @@ -2574,6 +2578,7 @@ mod tests { } /// A loader that immediately returns a [`TestAsset`]. + #[derive(TypePath)] struct TrivialLoader; impl AssetLoader for TrivialLoader { @@ -2652,6 +2657,7 @@ mod tests { #[derive(Asset, TypePath)] struct DeferredNested(Handle); + #[derive(TypePath)] struct DeferredNestedLoader; impl AssetLoader for DeferredNestedLoader { @@ -2679,6 +2685,7 @@ mod tests { #[derive(Asset, TypePath)] struct ImmediateNested(Handle); + #[derive(TypePath)] struct ImmediateNestedLoader; impl AssetLoader for ImmediateNestedLoader { diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index cf40e8840ad50..1a2714136f32d 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -14,6 +14,7 @@ use alloc::{ use atomicow::CowArc; use bevy_ecs::{error::BevyError, world::World}; use bevy_platform::collections::{HashMap, HashSet}; +use bevy_reflect::TypePath; use bevy_tasks::{BoxedFuture, ConditionalSendFuture}; use core::any::{Any, TypeId}; use downcast_rs::{impl_downcast, Downcast}; @@ -28,7 +29,7 @@ use thiserror::Error; /// This trait is generally used in concert with [`AssetReader`](crate::io::AssetReader) to load assets from a byte source. /// /// For a complementary version of this trait that can save assets, see [`AssetSaver`](crate::saver::AssetSaver). -pub trait AssetLoader: Send + Sync + 'static { +pub trait AssetLoader: TypePath + Send + Sync + 'static { /// The top level [`Asset`] loaded by this [`AssetLoader`]. type Asset: Asset; /// The settings type used by this [`AssetLoader`]. diff --git a/crates/bevy_asset/src/processor/mod.rs b/crates/bevy_asset/src/processor/mod.rs index 4fd56d9b42170..1a8dca22b5393 100644 --- a/crates/bevy_asset/src/processor/mod.rs +++ b/crates/bevy_asset/src/processor/mod.rs @@ -76,6 +76,7 @@ use tracing::{debug, error, trace, warn}; #[cfg(feature = "trace")] use { alloc::string::ToString, + bevy_reflect::TypePath, bevy_tasks::ConditionalSendFuture, tracing::{info_span, instrument::Instrument}, }; @@ -133,9 +134,9 @@ pub(crate) struct ProcessingState { #[derive(Default)] struct Processors { - /// Maps the type name of the processor to its instance. - type_name_to_processor: HashMap<&'static str, Arc>, - /// Maps the file extension of an asset to the type name of the processor we should use to + /// Maps the type path of the processor to its instance. + type_path_to_processor: HashMap<&'static str, Arc>, + /// Maps the file extension of an asset to the type path of the processor we should use to /// process it by default. file_extension_to_default_processor: HashMap, &'static str>, } @@ -728,8 +729,8 @@ impl AssetProcessor { #[cfg(feature = "trace")] let processor = InstrumentedAssetProcessor(processor); processors - .type_name_to_processor - .insert(core::any::type_name::

(), Arc::new(processor)); + .type_path_to_processor + .insert(P::type_path(), Arc::new(processor)); } /// Set the default processor for the given `extension`. Make sure `P` is registered with [`AssetProcessor::register_processor`]. @@ -741,7 +742,7 @@ impl AssetProcessor { .unwrap_or_else(PoisonError::into_inner); processors .file_extension_to_default_processor - .insert(extension.into(), core::any::type_name::

()); + .insert(extension.into(), P::type_path()); } /// Returns the default processor for the given `extension`, if it exists. @@ -754,7 +755,7 @@ impl AssetProcessor { let key = processors .file_extension_to_default_processor .get(extension)?; - processors.type_name_to_processor.get(key).cloned() + processors.type_path_to_processor.get(key).cloned() } /// Returns the processor with the given `processor_type_name`, if it exists. @@ -765,7 +766,7 @@ impl AssetProcessor { .read() .unwrap_or_else(PoisonError::into_inner); processors - .type_name_to_processor + .type_path_to_processor .get(processor_type_name) .cloned() } @@ -1422,6 +1423,7 @@ impl ProcessingState { } #[cfg(feature = "trace")] +#[derive(TypePath)] struct InstrumentedAssetProcessor(T); #[cfg(feature = "trace")] @@ -1445,7 +1447,7 @@ impl Process for InstrumentedAssetProcessor { }; let span = info_span!( "asset processing", - processor = core::any::type_name::(), + processor = T::type_path(), asset = context.path().to_string(), ); self.0.process(context, meta, writer).instrument(span) diff --git a/crates/bevy_asset/src/processor/process.rs b/crates/bevy_asset/src/processor/process.rs index a29b6abcab1a5..72c85d9c07e07 100644 --- a/crates/bevy_asset/src/processor/process.rs +++ b/crates/bevy_asset/src/processor/process.rs @@ -15,6 +15,7 @@ use alloc::{ boxed::Box, string::{String, ToString}, }; +use bevy_reflect::TypePath; use bevy_tasks::{BoxedFuture, ConditionalSendFuture}; use core::marker::PhantomData; use serde::{Deserialize, Serialize}; @@ -25,7 +26,7 @@ use thiserror::Error; /// /// This is a "low level", maximally flexible interface. Most use cases are better served by the [`LoadTransformAndSave`] implementation /// of [`Process`]. -pub trait Process: Send + Sync + Sized + 'static { +pub trait Process: TypePath + Send + Sync + Sized + 'static { /// The configuration / settings used to process the asset. This will be stored in the [`AssetMeta`] and is user-configurable per-asset. type Settings: Settings + Default + Serialize + for<'a> Deserialize<'a>; /// The [`AssetLoader`] that will be used to load the final processed asset. @@ -59,6 +60,7 @@ pub trait Process: Send + Sync + Sized + 'static { /// This uses [`LoadTransformAndSaveSettings`] to configure the processor. /// /// [`Asset`]: crate::Asset +#[derive(TypePath)] pub struct LoadTransformAndSave< L: AssetLoader, T: AssetTransformer, diff --git a/crates/bevy_asset/src/processor/tests.rs b/crates/bevy_asset/src/processor/tests.rs index c1c5f56d82e33..f06118b8c73fc 100644 --- a/crates/bevy_asset/src/processor/tests.rs +++ b/crates/bevy_asset/src/processor/tests.rs @@ -287,6 +287,7 @@ fn run_app_until_finished_processing(app: &mut App, guard: RwLockWriteGuard<'_, }); } +#[derive(TypePath)] struct CoolTextSaver; impl AssetSaver for CoolTextSaver { @@ -326,9 +327,10 @@ impl AssetSaver for CoolTextSaver { // Note: while we allow any Fn, since closures are unnameable types, creating a processor with a // closure cannot be used (since we need to include the name of the transformer in the meta // file). +#[derive(TypePath)] struct RootAssetTransformer, A: Asset>(M, PhantomData); -trait MutateAsset: Send + Sync + 'static { +trait MutateAsset: TypePath + Send + Sync + 'static { fn mutate(&self, asset: &mut A); } @@ -354,6 +356,7 @@ impl, A: Asset> AssetTransformer for RootAssetTransformer for AddText { @@ -529,6 +532,7 @@ struct FakeGltf { gltf_nodes: BTreeMap, } +#[derive(TypePath)] struct FakeGltfLoader; impl AssetLoader for FakeGltfLoader { @@ -565,6 +569,7 @@ struct FakeBsn { // scene that holds all the data including parents. // TODO: It would be nice if the inlining was actually done as an `AssetTransformer`, but // `Process` currently has no way to load nested assets. +#[derive(TypePath)] struct FakeBsnLoader; impl AssetLoader for FakeBsnLoader { @@ -1073,6 +1078,7 @@ fn nested_loads_of_processed_asset_reprocesses_on_reload() { value: String, } + #[derive(TypePath)] struct NesterLoader; impl AssetLoader for NesterLoader { @@ -1104,6 +1110,7 @@ fn nested_loads_of_processed_asset_reprocesses_on_reload() { } } + #[derive(TypePath)] struct AddTextToNested(String, Arc>); impl MutateAsset for AddTextToNested { @@ -1119,6 +1126,7 @@ fn nested_loads_of_processed_asset_reprocesses_on_reload() { ron::ser::to_string(&serialized).unwrap() } + #[derive(TypePath)] struct NesterSaver; impl AssetSaver for NesterSaver { diff --git a/crates/bevy_asset/src/saver.rs b/crates/bevy_asset/src/saver.rs index 0f3e3987f264a..86d170db02497 100644 --- a/crates/bevy_asset/src/saver.rs +++ b/crates/bevy_asset/src/saver.rs @@ -5,6 +5,7 @@ use crate::{ use alloc::boxed::Box; use atomicow::CowArc; use bevy_platform::collections::HashMap; +use bevy_reflect::TypePath; use bevy_tasks::{BoxedFuture, ConditionalSendFuture}; use core::{borrow::Borrow, hash::Hash, ops::Deref}; use serde::{Deserialize, Serialize}; @@ -15,7 +16,7 @@ use serde::{Deserialize, Serialize}; /// This trait is generally used in concert with [`AssetWriter`](crate::io::AssetWriter) to write assets as bytes. /// /// For a complementary version of this trait that can load assets, see [`AssetLoader`]. -pub trait AssetSaver: Send + Sync + 'static { +pub trait AssetSaver: TypePath + Send + Sync + 'static { /// The top level [`Asset`] saved by this [`AssetSaver`]. type Asset: Asset; /// The settings type used by this [`AssetSaver`]. diff --git a/crates/bevy_asset/src/server/loaders.rs b/crates/bevy_asset/src/server/loaders.rs index fe58c5ad36cd4..71fdd79165997 100644 --- a/crates/bevy_asset/src/server/loaders.rs +++ b/crates/bevy_asset/src/server/loaders.rs @@ -5,6 +5,8 @@ use crate::{ use alloc::{boxed::Box, sync::Arc, vec::Vec}; use async_broadcast::RecvError; use bevy_platform::collections::HashMap; +#[cfg(feature = "trace")] +use bevy_reflect::TypePath; use bevy_tasks::IoTaskPool; use bevy_utils::TypeIdMap; use core::any::TypeId; @@ -23,8 +25,8 @@ pub(crate) struct AssetLoaders { loaders: Vec, type_id_to_loaders: TypeIdMap>, extension_to_loaders: HashMap, Vec>, - type_name_to_loader: HashMap<&'static str, usize>, - preregistered_loaders: HashMap<&'static str, usize>, + type_path_to_loader: HashMap<&'static str, usize>, + type_path_to_preregistered_loader: HashMap<&'static str, usize>, } impl AssetLoaders { @@ -35,7 +37,7 @@ impl AssetLoaders { /// Registers a new [`AssetLoader`]. [`AssetLoader`]s must be registered before they can be used. pub(crate) fn push(&mut self, loader: L) { - let type_name = core::any::type_name::(); + let type_path = L::type_path(); let loader_asset_type = TypeId::of::(); let loader_asset_type_name = core::any::type_name::(); @@ -44,7 +46,7 @@ impl AssetLoaders { let loader = Arc::new(loader); let (loader_index, is_new) = - if let Some(index) = self.preregistered_loaders.remove(type_name) { + if let Some(index) = self.type_path_to_preregistered_loader.remove(type_path) { (index, false) } else { (self.loaders.len(), true) @@ -75,7 +77,7 @@ impl AssetLoaders { Loader must be specified in a .meta file in order to load assets of this type with these extensions."); } - self.type_name_to_loader.insert(type_name, loader_index); + self.type_path_to_loader.insert(type_path, loader_index); self.type_id_to_loaders .entry(loader_asset_type) @@ -108,12 +110,13 @@ impl AssetLoaders { pub(crate) fn reserve(&mut self, extensions: &[&str]) { let loader_asset_type = TypeId::of::(); let loader_asset_type_name = core::any::type_name::(); - let type_name = core::any::type_name::(); + let type_path = L::type_path(); let loader_index = self.loaders.len(); - self.preregistered_loaders.insert(type_name, loader_index); - self.type_name_to_loader.insert(type_name, loader_index); + self.type_path_to_preregistered_loader + .insert(type_path, loader_index); + self.type_path_to_loader.insert(type_path, loader_index); let existing_loaders_for_type_id = self.type_id_to_loaders.get(&loader_asset_type); let mut duplicate_extensions = Vec::new(); @@ -152,7 +155,7 @@ impl AssetLoaders { /// Get the [`AssetLoader`] by name pub(crate) fn get_by_name(&self, name: &str) -> Option { - let index = self.type_name_to_loader.get(name).copied()?; + let index = self.type_path_to_loader.get(name).copied()?; self.get_by_index(index) } @@ -309,6 +312,7 @@ impl MaybeAssetLoader { } #[cfg(feature = "trace")] +#[derive(TypePath)] struct InstrumentedAssetLoader(T); #[cfg(feature = "trace")] @@ -361,6 +365,7 @@ mod tests { #[derive(Asset, TypePath, Debug)] struct C; + #[derive(TypePath)] struct Loader { sender: Sender<()>, _phantom: PhantomData, @@ -430,7 +435,7 @@ mod tests { let loader = block_on( loaders - .get_by_name(core::any::type_name::>()) + .get_by_name(Loader::::type_path()) .unwrap() .get(), ) diff --git a/crates/bevy_asset/src/transformer.rs b/crates/bevy_asset/src/transformer.rs index f80f44511ab4c..575efbfbaa8eb 100644 --- a/crates/bevy_asset/src/transformer.rs +++ b/crates/bevy_asset/src/transformer.rs @@ -2,6 +2,7 @@ use crate::{meta::Settings, Asset, ErasedLoadedAsset, Handle, LabeledAsset, Unty use alloc::boxed::Box; use atomicow::CowArc; use bevy_platform::collections::HashMap; +use bevy_reflect::TypePath; use bevy_tasks::ConditionalSendFuture; use core::{ borrow::Borrow, @@ -15,7 +16,7 @@ use serde::{Deserialize, Serialize}; /// Transforms an [`Asset`] of a given [`AssetTransformer::AssetInput`] type to an [`Asset`] of [`AssetTransformer::AssetOutput`] type. /// /// This trait is commonly used in association with [`LoadTransformAndSave`](crate::processor::LoadTransformAndSave) to accomplish common asset pipeline workflows. -pub trait AssetTransformer: Send + Sync + 'static { +pub trait AssetTransformer: TypePath + Send + Sync + 'static { /// The [`Asset`] type which this [`AssetTransformer`] takes as and input. type AssetInput: Asset; /// The [`Asset`] type which this [`AssetTransformer`] outputs. @@ -249,6 +250,7 @@ impl<'a, A: Asset> TransformedSubAsset<'a, A> { } /// An identity [`AssetTransformer`] which infallibly returns the input [`Asset`] on transformation.] +#[derive(TypePath)] pub struct IdentityAssetTransformer { _phantom: PhantomData A>, } diff --git a/crates/bevy_audio/src/audio_source.rs b/crates/bevy_audio/src/audio_source.rs index 9cf3ca988b0b1..be54c89ea78ee 100644 --- a/crates/bevy_audio/src/audio_source.rs +++ b/crates/bevy_audio/src/audio_source.rs @@ -34,7 +34,7 @@ impl AsRef<[u8]> for AudioSource { /// `.mp3` with `bevy/mp3` /// `.flac` with `bevy/flac` /// `.wav` with `bevy/wav` -#[derive(Default)] +#[derive(Default, TypePath)] pub struct AudioLoader; impl AssetLoader for AudioLoader { diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index 73df951299f1b..34de9cbfed7b3 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -2,6 +2,7 @@ mod extensions; mod gltf_ext; use alloc::sync::Arc; +use bevy_reflect::TypePath; use std::{io::Error, sync::Mutex}; #[cfg(feature = "bevy_animation")] @@ -138,6 +139,7 @@ pub enum GltfError { } /// Loads glTF files with all of their data as their corresponding bevy representations. +#[derive(TypePath)] pub struct GltfLoader { /// List of compressed image formats handled by the loader. pub supported_compressed_formats: CompressedImageFormats, diff --git a/crates/bevy_image/src/compressed_image_saver.rs b/crates/bevy_image/src/compressed_image_saver.rs index 0c8f730630ce5..f5eb168ff8570 100644 --- a/crates/bevy_image/src/compressed_image_saver.rs +++ b/crates/bevy_image/src/compressed_image_saver.rs @@ -1,13 +1,15 @@ use crate::{Image, ImageFormat, ImageFormatSetting, ImageLoader, ImageLoaderSettings}; use bevy_asset::saver::{AssetSaver, SavedAsset}; +use bevy_reflect::TypePath; use futures_lite::AsyncWriteExt; use thiserror::Error; +#[derive(TypePath)] pub struct CompressedImageSaver; #[non_exhaustive] -#[derive(Debug, Error)] +#[derive(Debug, Error, TypePath)] pub enum CompressedImageSaverError { #[error(transparent)] Io(#[from] std::io::Error), diff --git a/crates/bevy_image/src/exr_texture_loader.rs b/crates/bevy_image/src/exr_texture_loader.rs index 9cbf315bb4f88..e40c9735dfb49 100644 --- a/crates/bevy_image/src/exr_texture_loader.rs +++ b/crates/bevy_image/src/exr_texture_loader.rs @@ -1,12 +1,13 @@ use crate::{Image, TextureAccessError, TextureFormatPixelInfo}; use bevy_asset::{io::Reader, AssetLoader, LoadContext, RenderAssetUsages}; +use bevy_reflect::TypePath; use image::ImageDecoder; use serde::{Deserialize, Serialize}; use thiserror::Error; use wgpu_types::{Extent3d, TextureDimension, TextureFormat}; /// Loads EXR textures as Texture assets -#[derive(Clone, Default)] +#[derive(Clone, Default, TypePath)] #[cfg(feature = "exr")] pub struct ExrTextureLoader; @@ -18,7 +19,7 @@ pub struct ExrTextureLoaderSettings { /// Possible errors that can be produced by [`ExrTextureLoader`] #[non_exhaustive] -#[derive(Debug, Error)] +#[derive(Debug, Error, TypePath)] #[cfg(feature = "exr")] pub enum ExrTextureLoaderError { #[error(transparent)] diff --git a/crates/bevy_image/src/hdr_texture_loader.rs b/crates/bevy_image/src/hdr_texture_loader.rs index 83e9df3b3d807..6210c91dccf87 100644 --- a/crates/bevy_image/src/hdr_texture_loader.rs +++ b/crates/bevy_image/src/hdr_texture_loader.rs @@ -1,13 +1,14 @@ use crate::{Image, TextureAccessError, TextureFormatPixelInfo}; use bevy_asset::RenderAssetUsages; use bevy_asset::{io::Reader, AssetLoader, LoadContext}; +use bevy_reflect::TypePath; use image::DynamicImage; use serde::{Deserialize, Serialize}; use thiserror::Error; use wgpu_types::{Extent3d, TextureDimension, TextureFormat}; /// Loads HDR textures as Texture assets -#[derive(Clone, Default)] +#[derive(Clone, Default, TypePath)] pub struct HdrTextureLoader; #[derive(Serialize, Deserialize, Default, Debug)] diff --git a/crates/bevy_image/src/image_loader.rs b/crates/bevy_image/src/image_loader.rs index 7f9616def65f5..522581e510c06 100644 --- a/crates/bevy_image/src/image_loader.rs +++ b/crates/bevy_image/src/image_loader.rs @@ -3,13 +3,14 @@ use crate::{ TextureReinterpretationError, }; use bevy_asset::{io::Reader, AssetLoader, LoadContext, RenderAssetUsages}; +use bevy_reflect::TypePath; use thiserror::Error; use super::{CompressedImageFormats, ImageSampler}; use serde::{Deserialize, Serialize}; /// Loader for images that can be read by the `image` crate. -#[derive(Clone)] +#[derive(Clone, TypePath)] pub struct ImageLoader { supported_compressed_formats: CompressedImageFormats, } diff --git a/crates/bevy_pbr/src/meshlet/asset.rs b/crates/bevy_pbr/src/meshlet/asset.rs index aebd2ddfbc526..38b0270326bd9 100644 --- a/crates/bevy_pbr/src/meshlet/asset.rs +++ b/crates/bevy_pbr/src/meshlet/asset.rs @@ -145,6 +145,7 @@ pub struct MeshletBoundingSphere { } /// An [`AssetSaver`] for `.meshlet_mesh` [`MeshletMesh`] assets. +#[derive(TypePath)] pub struct MeshletMeshSaver; impl AssetSaver for MeshletMeshSaver { @@ -193,6 +194,7 @@ impl AssetSaver for MeshletMeshSaver { } /// An [`AssetLoader`] for `.meshlet_mesh` [`MeshletMesh`] assets. +#[derive(TypePath)] pub struct MeshletMeshLoader; impl AssetLoader for MeshletMeshLoader { diff --git a/crates/bevy_scene/src/scene_loader.rs b/crates/bevy_scene/src/scene_loader.rs index 16b6015023a85..3c448e6f83403 100644 --- a/crates/bevy_scene/src/scene_loader.rs +++ b/crates/bevy_scene/src/scene_loader.rs @@ -2,7 +2,7 @@ use bevy_ecs::{ reflect::AppTypeRegistry, world::{FromWorld, World}, }; -use bevy_reflect::TypeRegistryArc; +use bevy_reflect::{TypePath, TypeRegistryArc}; use thiserror::Error; #[cfg(feature = "serialize")] @@ -15,7 +15,7 @@ use { /// Asset loader for a Bevy dynamic scene (`.scn` / `.scn.ron`). /// /// The loader handles assets serialized with [`DynamicScene::serialize`]. -#[derive(Debug)] +#[derive(Debug, TypePath)] pub struct SceneLoader { #[cfg_attr( not(feature = "serialize"), diff --git a/crates/bevy_shader/src/shader.rs b/crates/bevy_shader/src/shader.rs index 577fb06ee2e95..d6e7d9aac4113 100644 --- a/crates/bevy_shader/src/shader.rs +++ b/crates/bevy_shader/src/shader.rs @@ -338,7 +338,7 @@ impl From<&Source> for naga_oil::compose::ShaderType { } } -#[derive(Default)] +#[derive(Default, TypePath)] pub struct ShaderLoader; #[non_exhaustive] diff --git a/crates/bevy_text/src/font_loader.rs b/crates/bevy_text/src/font_loader.rs index 77b38082f2dfc..cc4da7b9804b8 100644 --- a/crates/bevy_text/src/font_loader.rs +++ b/crates/bevy_text/src/font_loader.rs @@ -1,9 +1,10 @@ use crate::Font; use bevy_asset::{io::Reader, AssetLoader, LoadContext}; +use bevy_reflect::TypePath; use cosmic_text::skrifa::raw::ReadError; use thiserror::Error; -#[derive(Default)] +#[derive(Default, TypePath)] /// An [`AssetLoader`] for [`Font`]s, for use by the [`AssetServer`](bevy_asset::AssetServer) pub struct FontLoader; diff --git a/examples/asset/asset_decompression.rs b/examples/asset/asset_decompression.rs index ce24bc16c1993..ef457612a32a1 100644 --- a/examples/asset/asset_decompression.rs +++ b/examples/asset/asset_decompression.rs @@ -17,7 +17,7 @@ struct GzAsset { uncompressed: ErasedLoadedAsset, } -#[derive(Default)] +#[derive(Default, TypePath)] struct GzAssetLoader; /// Possible errors that can be produced by [`GzAssetLoader`] diff --git a/examples/asset/custom_asset.rs b/examples/asset/custom_asset.rs index 8d4ac958ecdd1..60982e53fa313 100644 --- a/examples/asset/custom_asset.rs +++ b/examples/asset/custom_asset.rs @@ -17,7 +17,7 @@ struct CustomAsset { value: i32, } -#[derive(Default)] +#[derive(Default, TypePath)] struct CustomAssetLoader; /// Possible errors that can be produced by [`CustomAssetLoader`] @@ -58,7 +58,7 @@ struct Blob { bytes: Vec, } -#[derive(Default)] +#[derive(Default, TypePath)] struct BlobAssetLoader; /// Possible errors that can be produced by [`BlobAssetLoader`] diff --git a/examples/asset/processing/asset_processing.rs b/examples/asset/processing/asset_processing.rs index d5da644c27190..651dbe2148a76 100644 --- a/examples/asset/processing/asset_processing.rs +++ b/examples/asset/processing/asset_processing.rs @@ -69,7 +69,7 @@ impl Plugin for TextPlugin { #[derive(Asset, TypePath, Debug)] struct Text(String); -#[derive(Default)] +#[derive(Default, TypePath)] struct TextLoader; #[derive(Clone, Default, Serialize, Deserialize)] @@ -120,7 +120,7 @@ struct CoolText { dependencies: Vec>, } -#[derive(Default)] +#[derive(Default, TypePath)] struct CoolTextLoader; #[derive(Debug, Error)] @@ -182,7 +182,7 @@ impl AssetLoader for CoolTextLoader { } } -#[derive(Default)] +#[derive(Default, TypePath)] struct CoolTextTransformer; #[derive(Default, Serialize, Deserialize)] @@ -206,6 +206,7 @@ impl AssetTransformer for CoolTextTransformer { } } +#[derive(TypePath)] struct CoolTextSaver; impl AssetSaver for CoolTextSaver { From 67277800f19f3b411f3f8da8b72ab5b99676e3db Mon Sep 17 00:00:00 2001 From: andriyDev Date: Wed, 1 Oct 2025 23:08:13 -0700 Subject: [PATCH 03/14] Store the asset processors by their short type path as well and check the short path first. --- crates/bevy_asset/src/processor/mod.rs | 93 ++++++++++++++++++++-- crates/bevy_asset/src/processor/process.rs | 6 ++ 2 files changed, 92 insertions(+), 7 deletions(-) diff --git a/crates/bevy_asset/src/processor/mod.rs b/crates/bevy_asset/src/processor/mod.rs index 1a8dca22b5393..076a7359c5a46 100644 --- a/crates/bevy_asset/src/processor/mod.rs +++ b/crates/bevy_asset/src/processor/mod.rs @@ -56,10 +56,10 @@ use crate::{ AssetLoadError, AssetMetaCheck, AssetPath, AssetServer, AssetServerMode, DeserializeMetaError, MissingAssetLoaderForExtensionError, UnapprovedPathMode, WriteDefaultMetaError, }; -use alloc::{borrow::ToOwned, boxed::Box, sync::Arc, vec, vec::Vec}; +use alloc::{borrow::ToOwned, boxed::Box, string::String, sync::Arc, vec, vec::Vec}; use bevy_ecs::prelude::*; use bevy_platform::{ - collections::{HashMap, HashSet}, + collections::{hash_map::Entry, HashMap, HashSet}, sync::{PoisonError, RwLock}, }; use bevy_tasks::IoTaskPool; @@ -136,11 +136,27 @@ pub(crate) struct ProcessingState { struct Processors { /// Maps the type path of the processor to its instance. type_path_to_processor: HashMap<&'static str, Arc>, + /// Maps the short type path of the processor to its instance. + short_type_path_to_processor: HashMap<&'static str, ShortTypeProcessorEntry>, /// Maps the file extension of an asset to the type path of the processor we should use to /// process it by default. file_extension_to_default_processor: HashMap, &'static str>, } +enum ShortTypeProcessorEntry { + /// There is a unique processor with the given short type path. + Unique { + /// The full type path of the processor. + type_path: &'static str, + /// The processor itself. + processor: Arc, + }, + /// There are (at least) two processors with the same short type path (storing the full type + /// paths of all conflicting processors). Users must fully specify the type path in order to + /// disambiguate. + Ambiguous(Vec<&'static str>), +} + impl AssetProcessor { /// Creates a new [`AssetProcessor`] instance. pub fn new( @@ -728,9 +744,31 @@ impl AssetProcessor { .unwrap_or_else(PoisonError::into_inner); #[cfg(feature = "trace")] let processor = InstrumentedAssetProcessor(processor); + let processor = Arc::new(processor); processors .type_path_to_processor - .insert(P::type_path(), Arc::new(processor)); + .insert(P::type_path(), processor.clone()); + match processors + .short_type_path_to_processor + .entry(P::short_type_path()) + { + Entry::Vacant(entry) => { + entry.insert(ShortTypeProcessorEntry::Unique { + type_path: P::type_path(), + processor, + }); + } + Entry::Occupied(mut entry) => match entry.get_mut() { + ShortTypeProcessorEntry::Unique { type_path, .. } => { + let type_path = *type_path; + *entry.get_mut() = + ShortTypeProcessorEntry::Ambiguous(vec![type_path, P::type_path()]); + } + ShortTypeProcessorEntry::Ambiguous(type_paths) => { + type_paths.push(P::type_path()); + } + }, + } } /// Set the default processor for the given `extension`. Make sure `P` is registered with [`AssetProcessor::register_processor`]. @@ -759,16 +797,32 @@ impl AssetProcessor { } /// Returns the processor with the given `processor_type_name`, if it exists. - pub fn get_processor(&self, processor_type_name: &str) -> Option> { + pub fn get_processor( + &self, + processor_type_name: &str, + ) -> Result, GetProcessorError> { let processors = self .data .processors .read() .unwrap_or_else(PoisonError::into_inner); + if let Some(short_type_processor) = processors + .short_type_path_to_processor + .get(processor_type_name) + { + return match short_type_processor { + ShortTypeProcessorEntry::Unique { processor, .. } => Ok(processor.clone()), + ShortTypeProcessorEntry::Ambiguous(examples) => Err(GetProcessorError::Ambiguous { + processor_short_name: processor_type_name.to_owned(), + ambiguous_processor_names: examples.clone(), + }), + }; + } processors .type_path_to_processor .get(processor_type_name) .cloned() + .ok_or_else(|| GetProcessorError::Missing(processor_type_name.to_owned())) } /// Populates the initial view of each asset by scanning the unprocessed and processed asset folders. @@ -1001,9 +1055,7 @@ impl AssetProcessor { (meta, None) } AssetActionMinimal::Process { processor } => { - let processor = self - .get_processor(&processor) - .ok_or_else(|| ProcessError::MissingProcessor(processor))?; + let processor = self.get_processor(&processor)?; let meta = processor.deserialize_meta(&meta_bytes)?; (meta, Some(processor)) } @@ -1789,5 +1841,32 @@ pub enum SetTransactionLogFactoryError { AlreadyInUse, } +/// An error when retrieving an asset processor. +#[derive(Error, Debug)] +pub enum GetProcessorError { + #[error("The processor '{0}' does not exist")] + Missing(String), + #[error("The processor '{processor_short_name}' is ambiguous between several processors: {ambiguous_processor_names:?}")] + Ambiguous { + processor_short_name: String, + ambiguous_processor_names: Vec<&'static str>, + }, +} + +impl From for ProcessError { + fn from(err: GetProcessorError) -> Self { + match err { + GetProcessorError::Missing(name) => Self::MissingProcessor(name), + GetProcessorError::Ambiguous { + processor_short_name, + ambiguous_processor_names, + } => Self::AmbiguousProcessor { + processor_short_name, + ambiguous_processor_names, + }, + } + } +} + #[cfg(test)] mod tests; diff --git a/crates/bevy_asset/src/processor/process.rs b/crates/bevy_asset/src/processor/process.rs index 72c85d9c07e07..ca334405939be 100644 --- a/crates/bevy_asset/src/processor/process.rs +++ b/crates/bevy_asset/src/processor/process.rs @@ -14,6 +14,7 @@ use alloc::{ borrow::ToOwned, boxed::Box, string::{String, ToString}, + vec::Vec, }; use bevy_reflect::TypePath; use bevy_tasks::{BoxedFuture, ConditionalSendFuture}; @@ -122,6 +123,11 @@ pub enum ProcessError { #[error("The processor '{0}' does not exist")] #[from(ignore)] MissingProcessor(String), + #[error("The processor '{processor_short_name}' is ambiguous between several processors: {ambiguous_processor_names:?}")] + AmbiguousProcessor { + processor_short_name: String, + ambiguous_processor_names: Vec<&'static str>, + }, #[error("Encountered an AssetReader error for '{path}': {err}")] #[from(ignore)] AssetReaderError { From 40cca59029ccf144f08e825bff7df522e088b392 Mon Sep 17 00:00:00 2001 From: andriyDev Date: Thu, 2 Oct 2025 00:02:21 -0700 Subject: [PATCH 04/14] Change the asset_processing example file to use short paths instead of fully qualifying. --- examples/asset/processing/assets/a.cool.ron.meta | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/asset/processing/assets/a.cool.ron.meta b/examples/asset/processing/assets/a.cool.ron.meta index d87c629bac33d..bfe78b3128f5d 100644 --- a/examples/asset/processing/assets/a.cool.ron.meta +++ b/examples/asset/processing/assets/a.cool.ron.meta @@ -1,7 +1,7 @@ ( meta_format_version: "1.0", asset: Process( - processor: "bevy_asset::processor::process::LoadTransformAndSave", + processor: "LoadTransformAndSave", settings: ( loader_settings: (), transformer_settings: ( From 1c06c19a46a96be1491773135d535a50e9c3b0b9 Mon Sep 17 00:00:00 2001 From: andriyDev Date: Thu, 2 Oct 2025 00:36:08 -0700 Subject: [PATCH 05/14] Add a TODO to also allow short paths for loaders. --- crates/bevy_asset/src/server/loaders.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bevy_asset/src/server/loaders.rs b/crates/bevy_asset/src/server/loaders.rs index 71fdd79165997..37004a9a7c341 100644 --- a/crates/bevy_asset/src/server/loaders.rs +++ b/crates/bevy_asset/src/server/loaders.rs @@ -38,6 +38,7 @@ impl AssetLoaders { /// Registers a new [`AssetLoader`]. [`AssetLoader`]s must be registered before they can be used. pub(crate) fn push(&mut self, loader: L) { let type_path = L::type_path(); + // TODO: Allow using the short path of loaders. let loader_asset_type = TypeId::of::(); let loader_asset_type_name = core::any::type_name::(); @@ -111,6 +112,7 @@ impl AssetLoaders { let loader_asset_type = TypeId::of::(); let loader_asset_type_name = core::any::type_name::(); let type_path = L::type_path(); + // TODO: Allow using the short path of loaders. let loader_index = self.loaders.len(); From b14a4fd40b015c4830f34b0f2849a7de4e38b0bd Mon Sep 17 00:00:00 2001 From: andriyDev Date: Thu, 2 Oct 2025 00:38:09 -0700 Subject: [PATCH 06/14] Make the processor write type_path, and return a meta using type_path. --- crates/bevy_asset/src/processor/process.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bevy_asset/src/processor/process.rs b/crates/bevy_asset/src/processor/process.rs index ca334405939be..0c38d25e40f74 100644 --- a/crates/bevy_asset/src/processor/process.rs +++ b/crates/bevy_asset/src/processor/process.rs @@ -188,7 +188,7 @@ where return Err(ProcessError::WrongMetaType); }; let loader_meta = AssetMeta::::new(AssetAction::Load { - loader: core::any::type_name::().to_string(), + loader: Loader::type_path().to_string(), settings: settings.loader_settings, }); let pre_transformed_asset = TransformedAsset::::from_loaded( @@ -245,7 +245,7 @@ impl ErasedProcessor for P { let loader_settings =

::process(self, context, *meta, writer).await?; let output_meta: Box = Box::new(AssetMeta::::new(AssetAction::Load { - loader: core::any::type_name::().to_string(), + loader: P::OutputLoader::type_path().to_string(), settings: loader_settings, })); Ok(output_meta) @@ -259,7 +259,7 @@ impl ErasedProcessor for P { fn default_meta(&self) -> Box { Box::new(AssetMeta::<(), P>::new(AssetAction::Process { - processor: core::any::type_name::

().to_string(), + processor: P::type_path().to_string(), settings: P::Settings::default(), })) } @@ -315,7 +315,7 @@ impl<'a> ProcessContext<'a> { meta: AssetMeta, ) -> Result { let server = &self.processor.server; - let loader_name = core::any::type_name::(); + let loader_name = L::type_path(); let loader = server.get_asset_loader_with_type_name(loader_name).await?; let loaded_asset = server .load_with_meta_loader_and_reader( From 3ce2d3fdd1cf1667dadab7bdf0b1635d3bee2440 Mon Sep 17 00:00:00 2001 From: andriyDev Date: Thu, 2 Oct 2025 00:45:38 -0700 Subject: [PATCH 07/14] Use type_path in place of type_name where appropriate. --- crates/bevy_asset/src/loader.rs | 10 +++++----- crates/bevy_asset/src/loader_builders.rs | 2 +- crates/bevy_asset/src/server/loaders.rs | 4 ++-- crates/bevy_asset/src/server/mod.rs | 8 ++++---- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index 1a2714136f32d..e00e57cb27045 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -67,8 +67,8 @@ pub trait ErasedAssetLoader: Send + Sync + 'static { fn deserialize_meta(&self, meta: &[u8]) -> Result, DeserializeMetaError>; /// Returns the default meta value for the [`AssetLoader`] (erased as [`Box`]). fn default_meta(&self) -> Box; - /// Returns the type name of the [`AssetLoader`]. - fn type_name(&self) -> &'static str; + /// Returns the type path of the [`AssetLoader`]. + fn type_path(&self) -> &'static str; /// Returns the [`TypeId`] of the [`AssetLoader`]. fn type_id(&self) -> TypeId; /// Returns the type name of the top-level [`Asset`] loaded by the [`AssetLoader`]. @@ -112,13 +112,13 @@ where fn default_meta(&self) -> Box { Box::new(AssetMeta::::new(crate::meta::AssetAction::Load { - loader: self.type_name().to_string(), + loader: self.type_path().to_string(), settings: L::Settings::default(), })) } - fn type_name(&self) -> &'static str { - core::any::type_name::() + fn type_path(&self) -> &'static str { + L::type_path() } fn type_id(&self) -> TypeId { diff --git a/crates/bevy_asset/src/loader_builders.rs b/crates/bevy_asset/src/loader_builders.rs index 994eb33590bee..f842d26a50f81 100644 --- a/crates/bevy_asset/src/loader_builders.rs +++ b/crates/bevy_asset/src/loader_builders.rs @@ -481,7 +481,7 @@ impl NestedLoader<'_, '_, StaticTyped, Immediate<'_, '_>> { path, requested: TypeId::of::(), actual_asset_name: loader.asset_type_name(), - loader_name: loader.type_name(), + loader_name: loader.type_path(), }, }) }) diff --git a/crates/bevy_asset/src/server/loaders.rs b/crates/bevy_asset/src/server/loaders.rs index 37004a9a7c341..11e9d83c5af0d 100644 --- a/crates/bevy_asset/src/server/loaders.rs +++ b/crates/bevy_asset/src/server/loaders.rs @@ -331,7 +331,7 @@ impl AssetLoader for InstrumentedAssetLoader { ) -> impl ConditionalSendFuture> { let span = info_span!( "asset loading", - loader = core::any::type_name::(), + loader = T::type_path(), asset = load_context.path().to_string(), ); self.0.load(reader, settings, load_context).instrument(span) @@ -437,7 +437,7 @@ mod tests { let loader = block_on( loaders - .get_by_name(Loader::::type_path()) + .get_by_name( as TypePath>::type_path()) .unwrap() .get(), ) diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index 9f7d594e5d3bb..77a1cd55af3d4 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -252,7 +252,7 @@ impl AssetServer { loader.get().await.map_err(|_| error()) } - /// Returns the registered [`AssetLoader`] associated with the given [`core::any::type_name`], if it exists. + /// Returns the registered [`AssetLoader`] associated with the given type name, if it exists. pub async fn get_asset_loader_with_type_name( &self, type_name: &str, @@ -790,7 +790,7 @@ impl AssetServer { path: path.into_owned(), requested: asset_type_id, actual_asset_name: loader.asset_type_name(), - loader_name: loader.type_name(), + loader_name: loader.type_path(), }); } } @@ -1535,12 +1535,12 @@ impl AssetServer { .await .map_err(|_| AssetLoadError::AssetLoaderPanic { path: asset_path.clone_owned(), - loader_name: loader.type_name(), + loader_name: loader.type_path(), })? .map_err(|e| { AssetLoadError::AssetLoaderError(AssetLoaderError { path: asset_path.clone_owned(), - loader_name: loader.type_name(), + loader_name: loader.type_path(), error: e.into(), }) }) From 1b8aa3ea796c019cb681661e10ced124c3898281 Mon Sep 17 00:00:00 2001 From: andriyDev Date: Thu, 2 Oct 2025 18:39:15 -0700 Subject: [PATCH 08/14] Add a migration guide and release notes for short type path asset procesors. --- .../type_path_for_asset_traits.md | 25 +++++++++++ .../short_type_path_asset_processors.md | 44 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 release-content/migration-guides/type_path_for_asset_traits.md create mode 100644 release-content/release-notes/short_type_path_asset_processors.md diff --git a/release-content/migration-guides/type_path_for_asset_traits.md b/release-content/migration-guides/type_path_for_asset_traits.md new file mode 100644 index 0000000000000..eda0fd901735f --- /dev/null +++ b/release-content/migration-guides/type_path_for_asset_traits.md @@ -0,0 +1,25 @@ +--- +title: Traits `AssetLoader`, `AssetTransformer`, `AssetSaver`, and `Process` all now require `TypePath` +pull_requests: [21339] +--- + +The `AssetLoader`, `AssetTransformer`, `AssetSaver`, and `Process` traits now include a super trait +of `TypePath`. This means if you previously had a loader like: + +```rust +struct MyFunkyLoader { + add_funk: u32, +} +``` + +You will need to add the following derive: + +```rust +#[derive(TypePath)] +struct MyFunkyLoader { + add_funk: u32, +} +``` + +`TypePath` comes from `bevy_reflect`, so libraries may also need to add a dependency on +`bevy_reflect`. diff --git a/release-content/release-notes/short_type_path_asset_processors.md b/release-content/release-notes/short_type_path_asset_processors.md new file mode 100644 index 0000000000000..ab74ae7febb98 --- /dev/null +++ b/release-content/release-notes/short_type_path_asset_processors.md @@ -0,0 +1,44 @@ +--- +title: Short-type-path asset processors +authors: ["@andriyDev"] +pull_requests: [21339] +--- + +Asset processors allow manipulating assets at "publish-time" to convert them into a more optimal +form when loading the data at runtime. This can either be done using a default processor, which +processes all assets with a particular file extension, or by specifying the processor in the asset's +meta file. + +In previous versions of Bevy, the processor had to be **fully** specified in the asset's meta file. +For example: + +```ron +( + meta_format_version: "1.0", + asset: Process( + processor: "bevy_asset::processor::process::LoadTransformAndSave", + settings: ( + loader_settings: (), + transformer_settings: (), + saver_settings: (), + ), + ), +) +``` + +As you can see, processor types can be very verbose! In order to make these meta files easier to +manipulate, we now also support using the "short type path" of the asset. This would look like: + +```ron +( + meta_format_version: "1.0", + asset: Process( + processor: "LoadTransformAndSave", + settings: ( + loader_settings: (), + transformer_settings: (), + saver_settings: (), + ), + ), +) +``` From 16b90988d49e8823ec01cc9fadc43d8c7f101ea6 Mon Sep 17 00:00:00 2001 From: andriyDev Date: Thu, 2 Oct 2025 19:45:58 -0700 Subject: [PATCH 09/14] Add tests to ensure that short type paths can fetch the processor. --- crates/bevy_asset/src/processor/mod.rs | 2 +- crates/bevy_asset/src/processor/tests.rs | 135 ++++++++++++++++++++++- 2 files changed, 132 insertions(+), 5 deletions(-) diff --git a/crates/bevy_asset/src/processor/mod.rs b/crates/bevy_asset/src/processor/mod.rs index 076a7359c5a46..14ab04a30d96e 100644 --- a/crates/bevy_asset/src/processor/mod.rs +++ b/crates/bevy_asset/src/processor/mod.rs @@ -1842,7 +1842,7 @@ pub enum SetTransactionLogFactoryError { } /// An error when retrieving an asset processor. -#[derive(Error, Debug)] +#[derive(Error, Debug, PartialEq, Eq)] pub enum GetProcessorError { #[error("The processor '{0}' does not exist")] Missing(String), diff --git a/crates/bevy_asset/src/processor/tests.rs b/crates/bevy_asset/src/processor/tests.rs index f06118b8c73fc..9d2e2130fb4f5 100644 --- a/crates/bevy_asset/src/processor/tests.rs +++ b/crates/bevy_asset/src/processor/tests.rs @@ -25,12 +25,13 @@ use bevy_tasks::BoxedFuture; use crate::{ io::{ memory::{Dir, MemoryAssetReader, MemoryAssetWriter}, - AssetReader, AssetReaderError, AssetSourceBuilder, AssetSourceEvent, AssetSourceId, - AssetWatcher, PathStream, Reader, + AssetReader, AssetReaderError, AssetSourceBuilder, AssetSourceBuilders, AssetSourceEvent, + AssetSourceId, AssetWatcher, PathStream, Reader, }, + meta::AssetMeta, processor::{ - AssetProcessor, LoadTransformAndSave, LogEntry, ProcessorState, ProcessorTransactionLog, - ProcessorTransactionLogFactory, + AssetProcessor, GetProcessorError, LoadTransformAndSave, LogEntry, Process, ProcessContext, + ProcessError, ProcessorState, ProcessorTransactionLog, ProcessorTransactionLogFactory, }, saver::AssetSaver, tests::{run_app_until, CoolText, CoolTextLoader, CoolTextRon, SubText}, @@ -38,6 +39,132 @@ use crate::{ Asset, AssetApp, AssetLoader, AssetMode, AssetPath, AssetPlugin, LoadContext, }; +#[derive(TypePath)] +struct MyProcessor(PhantomData T>); + +impl Process for MyProcessor { + type OutputLoader = (); + type Settings = (); + + async fn process( + &self, + _context: &mut ProcessContext<'_>, + _meta: AssetMeta<(), Self>, + _writer: &mut crate::io::Writer, + ) -> Result<(), ProcessError> { + Ok(()) + } +} + +#[derive(TypePath)] +struct Marker; + +fn create_empty_asset_processor() -> AssetProcessor { + let mut sources = AssetSourceBuilders::default(); + // Create an empty asset source so that AssetProcessor is happy. + let dir = Dir::default(); + let memory_reader = MemoryAssetReader { root: dir.clone() }; + sources.insert( + AssetSourceId::Default, + AssetSourceBuilder::new(move || Box::new(memory_reader.clone())), + ); + + AssetProcessor::new(&mut sources, false).0 +} + +#[test] +fn get_asset_processor_by_name() { + let asset_processor = create_empty_asset_processor(); + asset_processor.register_processor(MyProcessor::(PhantomData)); + + let long_processor = asset_processor + .get_processor( + "bevy_asset::processor::tests::MyProcessor", + ) + .expect("Processor was previously registered"); + let short_processor = asset_processor + .get_processor("MyProcessor") + .expect("Processor was previously registered"); + + // We can use either the long or short processor name and we will get the same processor + // out. + assert!(Arc::ptr_eq(&long_processor, &short_processor)); +} + +#[test] +fn missing_processor_returns_error() { + let asset_processor = create_empty_asset_processor(); + + let Err(long_processor_err) = asset_processor.get_processor( + "bevy_asset::processor::tests::MyProcessor", + ) else { + panic!("Processor was returned even though we never registered any."); + }; + let GetProcessorError::Missing(long_processor_err) = &long_processor_err else { + panic!("get_processor returned incorrect error: {long_processor_err}"); + }; + assert_eq!( + long_processor_err, + "bevy_asset::processor::tests::MyProcessor" + ); + + // Short paths should also return an error. + + let Err(long_processor_err) = asset_processor.get_processor("MyProcessor") else { + panic!("Processor was returned even though we never registered any."); + }; + let GetProcessorError::Missing(long_processor_err) = &long_processor_err else { + panic!("get_processor returned incorrect error: {long_processor_err}"); + }; + assert_eq!(long_processor_err, "MyProcessor"); +} + +// Create another marker type whose short name will overlap `Marker`. +mod sneaky { + use bevy_reflect::TypePath; + + #[derive(TypePath)] + pub struct Marker; +} + +#[test] +fn ambiguous_short_path_returns_error() { + let asset_processor = create_empty_asset_processor(); + asset_processor.register_processor(MyProcessor::(PhantomData)); + asset_processor.register_processor(MyProcessor::(PhantomData)); + + let Err(long_processor_err) = asset_processor.get_processor("MyProcessor") else { + panic!("Processor was returned even though the short path is ambiguous."); + }; + let GetProcessorError::Ambiguous { + processor_short_name, + ambiguous_processor_names, + } = &long_processor_err + else { + panic!("get_processor returned incorrect error: {long_processor_err}"); + }; + assert_eq!(processor_short_name, "MyProcessor"); + let expected_ambiguous_names = [ + "bevy_asset::processor::tests::MyProcessor", + "bevy_asset::processor::tests::MyProcessor", + ]; + assert_eq!(ambiguous_processor_names, &expected_ambiguous_names); + + let processor_1 = asset_processor + .get_processor( + "bevy_asset::processor::tests::MyProcessor", + ) + .expect("Processor was previously registered"); + let processor_2 = asset_processor + .get_processor( + "bevy_asset::processor::tests::MyProcessor", + ) + .expect("Processor was previously registered"); + + // If we fully specify the paths, we get the two different processors. + assert!(!Arc::ptr_eq(&processor_1, &processor_2)); +} + #[derive(Clone)] struct ProcessingDirs { source: Dir, From 38f5658d562addbfc1338e6dfd19b286add174dc Mon Sep 17 00:00:00 2001 From: andriyDev Date: Mon, 6 Oct 2025 19:03:20 -0700 Subject: [PATCH 10/14] Add a test to ensure that using the short name actually results in the correct processing overall. --- crates/bevy_asset/src/processor/tests.rs | 64 ++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/crates/bevy_asset/src/processor/tests.rs b/crates/bevy_asset/src/processor/tests.rs index 9d2e2130fb4f5..d7c2ec3e772a4 100644 --- a/crates/bevy_asset/src/processor/tests.rs +++ b/crates/bevy_asset/src/processor/tests.rs @@ -654,6 +654,70 @@ fn asset_processor_transforms_asset_with_meta() { ); } +#[test] +fn asset_processor_transforms_asset_with_short_path_meta() { + let AppWithProcessor { + mut app, + source_gate, + default_source_dirs: + ProcessingDirs { + source: source_dir, + processed: processed_dir, + .. + }, + .. + } = create_app_with_asset_processor(&[]); + + type CoolTextProcessor = LoadTransformAndSave< + CoolTextLoader, + RootAssetTransformer, + CoolTextSaver, + >; + app.register_asset_loader(CoolTextLoader) + .register_asset_processor(CoolTextProcessor::new( + RootAssetTransformer::new(AddText("_def".into())), + CoolTextSaver, + )); + + let guard = source_gate.write_blocking(); + + let path = Path::new("abc.cool.ron"); + source_dir.insert_asset_text( + path, + r#"( + text: "abc", + dependencies: [], + embedded_dependencies: [], + sub_texts: [], +)"#, + ); + source_dir.insert_meta_text(path, r#"( + meta_format_version: "1.0", + asset: Process( + processor: "LoadTransformAndSave, CoolTextSaver>", + settings: ( + loader_settings: (), + transformer_settings: (), + saver_settings: (), + ), + ), +)"#); + + run_app_until_finished_processing(&mut app, guard); + + let processed_asset = processed_dir.get_asset(path).unwrap(); + let processed_asset = str::from_utf8(processed_asset.value()).unwrap(); + assert_eq!( + processed_asset, + r#"( + text: "abc_def", + dependencies: [], + embedded_dependencies: [], + sub_texts: [], +)"# + ); +} + #[derive(Asset, TypePath, Serialize, Deserialize)] struct FakeGltf { gltf_nodes: BTreeMap, From 9a2969a167217d11df4b44399ff06a55b88e570e Mon Sep 17 00:00:00 2001 From: andriyDev Date: Sun, 14 Dec 2025 00:03:01 -0800 Subject: [PATCH 11/14] Add TypePath derive to Gltf test. --- crates/bevy_gltf/src/loader/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index 34de9cbfed7b3..2c6e62d80ed37 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -1924,6 +1924,7 @@ mod test { use bevy_mesh::skinning::SkinnedMeshInverseBindposes; use bevy_mesh::MeshPlugin; use bevy_pbr::StandardMaterial; + use bevy_reflect::TypePath; use bevy_scene::ScenePlugin; fn test_app(dir: Dir) -> App { @@ -2460,6 +2461,7 @@ mod test { dir.insert_asset_text(Path::new("abc.png"), "Sup"); /// A fake loader to avoid actually loading any image data and just return an image. + #[derive(TypePath)] struct FakePngLoader; impl AssetLoader for FakePngLoader { From e3d03b9b45484ffbc26af3415ddd6a5d9ab5f158 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Mon, 15 Dec 2025 17:12:58 -0800 Subject: [PATCH 12/14] Derive TypePath for new test AssetLoaders --- crates/bevy_asset/src/lib.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index bc1927f6c1d78..4205f18212515 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -1987,6 +1987,7 @@ mod tests { // Extension "rsp" for Recursive Self Path (RSP). dir.insert_asset_text(Path::new("abc.rsp"), ""); + #[derive(TypePath)] struct ImmediateSelfLoader; impl AssetLoader for ImmediateSelfLoader { @@ -2041,6 +2042,7 @@ mod tests { dir.insert_asset_text(Path::new("abc.rsp"), ""); + #[derive(TypePath)] struct ImmediateSelfLoader; impl AssetLoader for ImmediateSelfLoader { @@ -2104,6 +2106,8 @@ mod tests { #[derive(Asset, TypePath)] pub struct TestAsset(Handle); + + #[derive(TypePath)] struct DeferredSelfLoader; impl AssetLoader for DeferredSelfLoader { @@ -2187,6 +2191,7 @@ mod tests { dir.insert_asset_text(Path::new("abc.rsp"), ""); + #[derive(TypePath)] struct ReadBytesSelfLoader; impl AssetLoader for ReadBytesSelfLoader { @@ -2271,6 +2276,8 @@ mod tests { #[derive(Asset, TypePath)] pub struct TestAssetUD(Handle); + + #[derive(TypePath)] struct ImmediateSelfLoader; impl AssetLoader for ImmediateSelfLoader { From c6778240ea70a38662544ace63a3d7258a758229 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Mon, 15 Dec 2025 17:13:56 -0800 Subject: [PATCH 13/14] Use short_type_path for ErasedProcessor::default_meta --- crates/bevy_asset/src/processor/process.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_asset/src/processor/process.rs b/crates/bevy_asset/src/processor/process.rs index 0c38d25e40f74..2a1408a3499d0 100644 --- a/crates/bevy_asset/src/processor/process.rs +++ b/crates/bevy_asset/src/processor/process.rs @@ -259,7 +259,7 @@ impl ErasedProcessor for P { fn default_meta(&self) -> Box { Box::new(AssetMeta::<(), P>::new(AssetAction::Process { - processor: P::type_path().to_string(), + processor: P::short_type_path().to_string(), settings: P::Settings::default(), })) } From c6c3fc54b4b43d915b64407dbe20a32aae9c42b6 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Mon, 15 Dec 2025 17:45:39 -0800 Subject: [PATCH 14/14] Revert "Use short_type_path for ErasedProcessor::default_meta" This reverts commit c6778240ea70a38662544ace63a3d7258a758229. --- crates/bevy_asset/src/processor/process.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_asset/src/processor/process.rs b/crates/bevy_asset/src/processor/process.rs index 2a1408a3499d0..0c38d25e40f74 100644 --- a/crates/bevy_asset/src/processor/process.rs +++ b/crates/bevy_asset/src/processor/process.rs @@ -259,7 +259,7 @@ impl ErasedProcessor for P { fn default_meta(&self) -> Box { Box::new(AssetMeta::<(), P>::new(AssetAction::Process { - processor: P::short_type_path().to_string(), + processor: P::type_path().to_string(), settings: P::Settings::default(), })) }