bevy_asset/processor/
process.rs

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