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