bevy_asset/processor/
process.rs

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