bevy_asset/processor/
process.rs

1use crate::{
2    io::{
3        AssetReaderError, AssetWriterError, MissingAssetWriterError,
4        MissingProcessedAssetReaderError, MissingProcessedAssetWriterError, SliceReader, Writer,
5    },
6    meta::{AssetAction, AssetMeta, AssetMetaDyn, ProcessDependencyInfo, ProcessedInfo, Settings},
7    processor::AssetProcessor,
8    saver::{AssetSaver, SavedAsset},
9    transformer::{AssetTransformer, IdentityAssetTransformer, TransformedAsset},
10    AssetLoadError, AssetLoader, AssetPath, DeserializeMetaError, ErasedLoadedAsset,
11    MissingAssetLoaderForExtensionError, MissingAssetLoaderForTypeNameError,
12};
13use bevy_utils::{BoxedFuture, ConditionalSendFuture};
14use core::marker::PhantomData;
15use derive_more::derive::{Display, Error, From};
16use serde::{Deserialize, Serialize};
17
18/// Asset "processor" logic that reads input asset bytes (stored on [`ProcessContext`]), processes the value in some way,
19/// and then writes the final processed bytes with [`Writer`]. The resulting bytes must be loadable with the given [`Process::OutputLoader`].
20///
21/// This is a "low level", maximally flexible interface. Most use cases are better served by the [`LoadTransformAndSave`] implementation
22/// of [`Process`].
23pub trait Process: Send + Sync + Sized + 'static {
24    /// The configuration / settings used to process the asset. This will be stored in the [`AssetMeta`] and is user-configurable per-asset.
25    type Settings: Settings + Default + Serialize + for<'a> Deserialize<'a>;
26    /// The [`AssetLoader`] that will be used to load the final processed asset.
27    type OutputLoader: AssetLoader;
28    /// Processes the asset stored on `context` in some way using the settings stored on `meta`. The results are written to `writer`. The
29    /// final written processed asset is loadable using [`Process::OutputLoader`]. This load will use the returned [`AssetLoader::Settings`].
30    fn process(
31        &self,
32        context: &mut ProcessContext,
33        meta: AssetMeta<(), Self>,
34        writer: &mut Writer,
35    ) -> impl ConditionalSendFuture<
36        Output = Result<<Self::OutputLoader as AssetLoader>::Settings, ProcessError>,
37    >;
38}
39
40/// A flexible [`Process`] implementation that loads the source [`Asset`] using the `L` [`AssetLoader`], then transforms
41/// the `L` asset into an `S` [`AssetSaver`] asset using the `T` [`AssetTransformer`], and lastly saves the asset using the `S` [`AssetSaver`].
42///
43/// When creating custom processors, it is generally recommended to use the [`LoadTransformAndSave`] [`Process`] implementation,
44/// as it encourages you to separate your code into an [`AssetLoader`] capable of loading assets without processing enabled,
45/// an [`AssetTransformer`] capable of converting from an `L` asset to an `S` asset, and
46/// an [`AssetSaver`] that allows you save any `S` asset. However you can
47/// also implement [`Process`] directly if [`LoadTransformAndSave`] feels limiting or unnecessary.
48///
49/// If your [`Process`] does not need to transform the [`Asset`], you can use [`IdentityAssetTransformer`] as `T`.
50/// This will directly return the input [`Asset`], allowing your [`Process`] to directly load and then save an [`Asset`].
51/// However, this pattern should only be used for cases such as file format conversion.
52/// Otherwise, consider refactoring your [`AssetLoader`] and [`AssetSaver`] to isolate the transformation step into an explicit [`AssetTransformer`].
53///
54/// This uses [`LoadTransformAndSaveSettings`] to configure the processor.
55///
56/// [`Asset`]: crate::Asset
57pub struct LoadTransformAndSave<
58    L: AssetLoader,
59    T: AssetTransformer<AssetInput = L::Asset>,
60    S: AssetSaver<Asset = T::AssetOutput>,
61> {
62    transformer: T,
63    saver: S,
64    marker: PhantomData<fn() -> L>,
65}
66
67impl<L: AssetLoader, S: AssetSaver<Asset = L::Asset>> From<S>
68    for LoadTransformAndSave<L, IdentityAssetTransformer<L::Asset>, S>
69{
70    fn from(value: S) -> Self {
71        LoadTransformAndSave {
72            transformer: IdentityAssetTransformer::new(),
73            saver: value,
74            marker: PhantomData,
75        }
76    }
77}
78
79/// Settings for the [`LoadTransformAndSave`] [`Process::Settings`] implementation.
80///
81/// `LoaderSettings` corresponds to [`AssetLoader::Settings`], `TransformerSettings` corresponds to [`AssetTransformer::Settings`],
82/// and `SaverSettings` corresponds to [`AssetSaver::Settings`].
83#[derive(Serialize, Deserialize, Default)]
84pub struct LoadTransformAndSaveSettings<LoaderSettings, TransformerSettings, SaverSettings> {
85    /// The [`AssetLoader::Settings`] for [`LoadTransformAndSave`].
86    pub loader_settings: LoaderSettings,
87    /// The [`AssetTransformer::Settings`] for [`LoadTransformAndSave`].
88    pub transformer_settings: TransformerSettings,
89    /// The [`AssetSaver::Settings`] for [`LoadTransformAndSave`].
90    pub saver_settings: SaverSettings,
91}
92
93impl<
94        L: AssetLoader,
95        T: AssetTransformer<AssetInput = L::Asset>,
96        S: AssetSaver<Asset = T::AssetOutput>,
97    > LoadTransformAndSave<L, T, S>
98{
99    pub fn new(transformer: T, saver: S) -> Self {
100        LoadTransformAndSave {
101            transformer,
102            saver,
103            marker: PhantomData,
104        }
105    }
106}
107
108/// A flexible [`Process`] implementation that loads the source [`Asset`] using the `L` [`AssetLoader`], then
109/// saves that `L` asset using the `S` [`AssetSaver`].
110///
111/// This is a specialized use case of [`LoadTransformAndSave`] and is useful where there is no asset manipulation
112/// such as when compressing assets.
113///
114/// This uses [`LoadAndSaveSettings`] to configure the processor.
115///
116/// [`Asset`]: crate::Asset
117#[deprecated = "Use `LoadTransformAndSave<L, IdentityAssetTransformer<<L as AssetLoader>::Asset>, S>` instead"]
118pub type LoadAndSave<L, S> =
119    LoadTransformAndSave<L, IdentityAssetTransformer<<L as AssetLoader>::Asset>, S>;
120
121/// Settings for the [`LoadAndSave`] [`Process::Settings`] implementation.
122///
123/// `LoaderSettings` corresponds to [`AssetLoader::Settings`] and `SaverSettings` corresponds to [`AssetSaver::Settings`].
124#[deprecated = "Use `LoadTransformAndSaveSettings<LoaderSettings, (), SaverSettings>` instead"]
125pub type LoadAndSaveSettings<LoaderSettings, SaverSettings> =
126    LoadTransformAndSaveSettings<LoaderSettings, (), SaverSettings>;
127
128/// An error that is encountered during [`Process::process`].
129#[derive(Error, Display, Debug, From)]
130pub enum ProcessError {
131    MissingAssetLoaderForExtension(MissingAssetLoaderForExtensionError),
132    MissingAssetLoaderForTypeName(MissingAssetLoaderForTypeNameError),
133    #[display("The processor '{_0}' does not exist")]
134    #[error(ignore)]
135    #[from(ignore)]
136    MissingProcessor(String),
137    #[display("Encountered an AssetReader error for '{path}': {err}")]
138    #[from(ignore)]
139    AssetReaderError {
140        path: AssetPath<'static>,
141        err: AssetReaderError,
142    },
143    #[display("Encountered an AssetWriter error for '{path}': {err}")]
144    #[from(ignore)]
145    AssetWriterError {
146        path: AssetPath<'static>,
147        err: AssetWriterError,
148    },
149    MissingAssetWriterError(MissingAssetWriterError),
150    MissingProcessedAssetReaderError(MissingProcessedAssetReaderError),
151    MissingProcessedAssetWriterError(MissingProcessedAssetWriterError),
152    #[display("Failed to read asset metadata for {path}: {err}")]
153    #[from(ignore)]
154    ReadAssetMetaError {
155        path: AssetPath<'static>,
156        err: AssetReaderError,
157    },
158    DeserializeMetaError(DeserializeMetaError),
159    AssetLoadError(AssetLoadError),
160    #[display("The wrong meta type was passed into a processor. This is probably an internal implementation error.")]
161    WrongMetaType,
162    #[display("Encountered an error while saving the asset: {_0}")]
163    #[from(ignore)]
164    AssetSaveError(Box<dyn core::error::Error + Send + Sync + 'static>),
165    #[display("Encountered an error while transforming the asset: {_0}")]
166    #[from(ignore)]
167    AssetTransformError(Box<dyn core::error::Error + Send + Sync + 'static>),
168    #[display("Assets without extensions are not supported.")]
169    ExtensionRequired,
170}
171
172impl<Loader, Transformer, Saver> Process for LoadTransformAndSave<Loader, Transformer, Saver>
173where
174    Loader: AssetLoader,
175    Transformer: AssetTransformer<AssetInput = Loader::Asset>,
176    Saver: AssetSaver<Asset = Transformer::AssetOutput>,
177{
178    type Settings =
179        LoadTransformAndSaveSettings<Loader::Settings, Transformer::Settings, Saver::Settings>;
180    type OutputLoader = Saver::OutputLoader;
181
182    async fn process(
183        &self,
184        context: &mut ProcessContext<'_>,
185        meta: AssetMeta<(), Self>,
186        writer: &mut Writer,
187    ) -> Result<<Self::OutputLoader as AssetLoader>::Settings, ProcessError> {
188        let AssetAction::Process { settings, .. } = meta.asset else {
189            return Err(ProcessError::WrongMetaType);
190        };
191        let loader_meta = AssetMeta::<Loader, ()>::new(AssetAction::Load {
192            loader: core::any::type_name::<Loader>().to_string(),
193            settings: settings.loader_settings,
194        });
195        let pre_transformed_asset = TransformedAsset::<Loader::Asset>::from_loaded(
196            context.load_source_asset(loader_meta).await?,
197        )
198        .unwrap();
199
200        let post_transformed_asset = self
201            .transformer
202            .transform(pre_transformed_asset, &settings.transformer_settings)
203            .await
204            .map_err(|err| ProcessError::AssetTransformError(err.into()))?;
205
206        let saved_asset =
207            SavedAsset::<Transformer::AssetOutput>::from_transformed(&post_transformed_asset);
208
209        let output_settings = self
210            .saver
211            .save(writer, saved_asset, &settings.saver_settings)
212            .await
213            .map_err(|error| ProcessError::AssetSaveError(error.into()))?;
214        Ok(output_settings)
215    }
216}
217
218/// A type-erased variant of [`Process`] that enables interacting with processor implementations without knowing
219/// their type.
220pub trait ErasedProcessor: Send + Sync {
221    /// Type-erased variant of [`Process::process`].
222    fn process<'a>(
223        &'a self,
224        context: &'a mut ProcessContext,
225        meta: Box<dyn AssetMetaDyn>,
226        writer: &'a mut Writer,
227    ) -> BoxedFuture<'a, Result<Box<dyn AssetMetaDyn>, ProcessError>>;
228    /// Deserialized `meta` as type-erased [`AssetMeta`], operating under the assumption that it matches the meta
229    /// for the underlying [`Process`] impl.
230    fn deserialize_meta(&self, meta: &[u8]) -> Result<Box<dyn AssetMetaDyn>, DeserializeMetaError>;
231    /// Returns the default type-erased [`AssetMeta`] for the underlying [`Process`] impl.
232    fn default_meta(&self) -> Box<dyn AssetMetaDyn>;
233}
234
235impl<P: Process> ErasedProcessor for P {
236    fn process<'a>(
237        &'a self,
238        context: &'a mut ProcessContext,
239        meta: Box<dyn AssetMetaDyn>,
240        writer: &'a mut Writer,
241    ) -> BoxedFuture<'a, Result<Box<dyn AssetMetaDyn>, ProcessError>> {
242        Box::pin(async move {
243            let meta = meta
244                .downcast::<AssetMeta<(), P>>()
245                .map_err(|_e| ProcessError::WrongMetaType)?;
246            let loader_settings = <P as Process>::process(self, context, *meta, writer).await?;
247            let output_meta: Box<dyn AssetMetaDyn> =
248                Box::new(AssetMeta::<P::OutputLoader, ()>::new(AssetAction::Load {
249                    loader: core::any::type_name::<P::OutputLoader>().to_string(),
250                    settings: loader_settings,
251                }));
252            Ok(output_meta)
253        })
254    }
255
256    fn deserialize_meta(&self, meta: &[u8]) -> Result<Box<dyn AssetMetaDyn>, DeserializeMetaError> {
257        let meta: AssetMeta<(), P> = ron::de::from_bytes(meta)?;
258        Ok(Box::new(meta))
259    }
260
261    fn default_meta(&self) -> Box<dyn AssetMetaDyn> {
262        Box::new(AssetMeta::<(), P>::new(AssetAction::Process {
263            processor: core::any::type_name::<P>().to_string(),
264            settings: P::Settings::default(),
265        }))
266    }
267}
268
269/// Provides scoped data access to the [`AssetProcessor`].
270/// This must only expose processor data that is represented in the asset's hash.
271pub struct ProcessContext<'a> {
272    /// The "new" processed info for the final processed asset. It is [`ProcessContext`]'s
273    /// job to populate `process_dependencies` with any asset dependencies used to process
274    /// this asset (ex: loading an asset value from the [`AssetServer`] of the [`AssetProcessor`])
275    ///
276    /// DO NOT CHANGE ANY VALUES HERE OTHER THAN APPENDING TO `process_dependencies`
277    ///
278    /// Do not expose this publicly as it would be too easily to invalidate state.
279    ///
280    /// [`AssetServer`]: crate::server::AssetServer
281    pub(crate) new_processed_info: &'a mut ProcessedInfo,
282    /// This exists to expose access to asset values (via the [`AssetServer`]).
283    ///
284    /// ANY ASSET VALUE THAT IS ACCESSED SHOULD BE ADDED TO `new_processed_info.process_dependencies`
285    ///
286    /// Do not expose this publicly as it would be too easily to invalidate state by forgetting to update
287    /// `process_dependencies`.
288    ///
289    /// [`AssetServer`]: crate::server::AssetServer
290    processor: &'a AssetProcessor,
291    path: &'a AssetPath<'static>,
292    asset_bytes: &'a [u8],
293}
294
295impl<'a> ProcessContext<'a> {
296    pub(crate) fn new(
297        processor: &'a AssetProcessor,
298        path: &'a AssetPath<'static>,
299        asset_bytes: &'a [u8],
300        new_processed_info: &'a mut ProcessedInfo,
301    ) -> Self {
302        Self {
303            processor,
304            path,
305            asset_bytes,
306            new_processed_info,
307        }
308    }
309
310    /// Load the source asset using the `L` [`AssetLoader`] and the passed in `meta` config.
311    /// This will take the "load dependencies" (asset values used when loading with `L`]) and
312    /// register them as "process dependencies" because they are asset values required to process the
313    /// current asset.
314    pub async fn load_source_asset<L: AssetLoader>(
315        &mut self,
316        meta: AssetMeta<L, ()>,
317    ) -> Result<ErasedLoadedAsset, AssetLoadError> {
318        let server = &self.processor.server;
319        let loader_name = core::any::type_name::<L>();
320        let loader = server.get_asset_loader_with_type_name(loader_name).await?;
321        let mut reader = SliceReader::new(self.asset_bytes);
322        let loaded_asset = server
323            .load_with_meta_loader_and_reader(
324                self.path,
325                Box::new(meta),
326                &*loader,
327                &mut reader,
328                false,
329                true,
330            )
331            .await?;
332        for (path, full_hash) in &loaded_asset.loader_dependencies {
333            self.new_processed_info
334                .process_dependencies
335                .push(ProcessDependencyInfo {
336                    full_hash: *full_hash,
337                    path: path.to_owned(),
338                });
339        }
340        Ok(loaded_asset)
341    }
342
343    /// The path of the asset being processed.
344    #[inline]
345    pub fn path(&self) -> &AssetPath<'static> {
346        self.path
347    }
348
349    /// The source bytes of the asset being processed.
350    #[inline]
351    pub fn asset_bytes(&self) -> &[u8] {
352        self.asset_bytes
353    }
354}