AssetSaver and AssetTransformer split (#11260)

# Objective
One of a few Bevy Asset improvements I would like to make: #11216.

Currently asset processing and asset saving are handled by the same
trait, `AssetSaver`. This makes it difficult to reuse saving
implementations and impossible to have a single "universal" saver for a
given asset type.

## Solution
This PR splits off the processing portion of `AssetSaver` into
`AssetTransformer`, which is responsible for transforming assets. This
change involves adding the `LoadTransformAndSave` processor, which
utilizes the new API. The `LoadAndSave` still exists since it remains
useful in situations where no "transformation" of the asset is done,
such as when compressing assets.

## Notes:
As an aside, Bikeshedding is welcome on the names. I'm not entirely
convinced by `AssetTransformer`, which was chosen mostly because
`AssetProcessor` is taken. Additionally, `LoadTransformSave` may be
sufficient instead of `LoadTransformAndSave`.


---

## Changelog
### Added 
- `AssetTransformer` which is responsible for transforming Assets.
- `LoadTransformAndSave`, a `Process` implementation.
### Changed
- Changed `AssetSaver`'s responsibilities from processing and saving to
just saving.
- Updated `asset_processing` example to use new API.
- Old asset .meta files regenerated with new processor.
This commit is contained in:
thepackett 2024-01-26 14:20:58 -06:00 committed by GitHub
parent 35ac1b152e
commit 76682fdcb7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 174 additions and 32 deletions

View file

@ -2,6 +2,7 @@ pub mod io;
pub mod meta;
pub mod processor;
pub mod saver;
pub mod transformer;
pub mod prelude {
#[doc(hidden)]

View file

@ -527,11 +527,11 @@ impl<'a> LoadContext<'a> {
/// deriving a new asset from the referenced asset, or you are building a collection of assets. This will add the `path` as a
/// "load dependency".
///
/// If the current loader is used in a [`Process`] "asset preprocessor", such as a [`LoadAndSave`] preprocessor,
/// If the current loader is used in a [`Process`] "asset preprocessor", such as a [`LoadTransformAndSave`] preprocessor,
/// changing a "load dependency" will result in re-processing of the asset.
///
/// [`Process`]: crate::processor::Process
/// [`LoadAndSave`]: crate::processor::LoadAndSave
/// [`LoadTransformAndSave`]: crate::processor::LoadTransformAndSave
pub async fn load_direct<'b>(
&mut self,
path: impl Into<AssetPath<'b>>,
@ -575,11 +575,11 @@ impl<'a> LoadContext<'a> {
/// For example, if you are deriving a new asset from the referenced asset, or you are building a collection of assets. This will add the `path` as a
/// "load dependency".
///
/// If the current loader is used in a [`Process`] "asset preprocessor", such as a [`LoadAndSave`] preprocessor,
/// If the current loader is used in a [`Process`] "asset preprocessor", such as a [`LoadTransformAndSave`] preprocessor,
/// changing a "load dependency" will result in re-processing of the asset.
///
/// [`Process`]: crate::processor::Process
/// [`LoadAndSave`]: crate::processor::LoadAndSave
/// [`LoadTransformAndSave`]: crate::processor::LoadTransformAndSave
pub async fn load_direct_with_reader<'b>(
&mut self,
reader: &mut Reader<'_>,

View file

@ -6,7 +6,8 @@ use crate::{
meta::{AssetAction, AssetMeta, AssetMetaDyn, ProcessDependencyInfo, ProcessedInfo, Settings},
processor::AssetProcessor,
saver::{AssetSaver, SavedAsset},
AssetLoadError, AssetLoader, AssetPath, DeserializeMetaError, ErasedLoadedAsset,
transformer::AssetTransformer,
AssetLoadError, AssetLoader, AssetPath, DeserializeMetaError, ErasedLoadedAsset, LoadedAsset,
MissingAssetLoaderForExtensionError, MissingAssetLoaderForTypeNameError,
};
use bevy_utils::BoxedFuture;
@ -17,7 +18,7 @@ use thiserror::Error;
/// Asset "processor" logic that reads input asset bytes (stored on [`ProcessContext`]), processes the value in some way,
/// and then writes the final processed bytes with [`Writer`]. The resulting bytes must be loadable with the given [`Process::OutputLoader`].
///
/// This is a "low level", maximally flexible interface. Most use cases are better served by the [`LoadAndSave`] implementation
/// 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 {
/// The configuration / settings used to process the asset. This will be stored in the [`AssetMeta`] and is user-configurable per-asset.
@ -34,13 +35,62 @@ pub trait Process: Send + Sync + Sized + 'static {
) -> BoxedFuture<'a, Result<<Self::OutputLoader as AssetLoader>::Settings, ProcessError>>;
}
/// A flexible [`Process`] implementation that loads the source [`Asset`] using the `L` [`AssetLoader`], then transforms
/// the `L` asset into an `S` [`AssetSaver`] asset using the `T` [`AssetTransformer`], and lastly saves the asset using the `S` [`AssetSaver`].
///
/// When creating custom processors, it is generally recommended to use the [`LoadTransformAndSave`] [`Process`] implementation,
/// as it encourages you to separate your code into an [`AssetLoader`] capable of loading assets without processing enabled,
/// an [`AssetTransformer`] capable of converting from an `L` asset to an `S` asset, and
/// an [`AssetSaver`] that allows you save any `S` asset. However you can
/// also implement [`Process`] directly if [`LoadTransformAndSave`] feels limiting or unnecessary.
///
/// This uses [`LoadTransformAndSaveSettings`] to configure the processor.
///
/// [`Asset`]: crate::Asset
pub struct LoadTransformAndSave<
L: AssetLoader,
T: AssetTransformer<AssetInput = L::Asset>,
S: AssetSaver<Asset = T::AssetOutput>,
> {
transformer: T,
saver: S,
marker: PhantomData<fn() -> L>,
}
/// Settings for the [`LoadTransformAndSave`] [`Process::Settings`] implementation.
///
/// `LoaderSettings` corresponds to [`AssetLoader::Settings`], `TransformerSettings` corresponds to [`AssetTransformer::Settings`],
/// and `SaverSettings` corresponds to [`AssetSaver::Settings`].
#[derive(Serialize, Deserialize, Default)]
pub struct LoadTransformAndSaveSettings<LoaderSettings, TransformerSettings, SaverSettings> {
/// The [`AssetLoader::Settings`] for [`LoadTransformAndSave`].
pub loader_settings: LoaderSettings,
/// The [`AssetTransformer::Settings`] for [`LoadTransformAndSave`].
pub transformer_settings: TransformerSettings,
/// The [`AssetSaver::Settings`] for [`LoadTransformAndSave`].
pub saver_settings: SaverSettings,
}
impl<
L: AssetLoader,
T: AssetTransformer<AssetInput = L::Asset>,
S: AssetSaver<Asset = T::AssetOutput>,
> LoadTransformAndSave<L, T, S>
{
pub fn new(transformer: T, saver: S) -> Self {
LoadTransformAndSave {
transformer,
saver,
marker: PhantomData,
}
}
}
/// A flexible [`Process`] implementation that loads the source [`Asset`] using the `L` [`AssetLoader`], then
/// saves that `L` asset using the `S` [`AssetSaver`].
///
/// When creating custom processors, it is generally recommended to use the [`LoadAndSave`] [`Process`] implementation,
/// as it encourages you to write both an [`AssetLoader`] capable of loading assets without processing enabled _and_
/// an [`AssetSaver`] that allows you to efficiently process that asset type when that is desirable by users. However you can
/// also implement [`Process`] directly if [`LoadAndSave`] feels limiting or unnecessary.
/// This is a specialized use case of [`LoadTransformAndSave`] and is useful where there is no asset manipulation
/// such as when compressing assets.
///
/// This uses [`LoadAndSaveSettings`] to configure the processor.
///
@ -112,6 +162,52 @@ pub enum ProcessError {
ExtensionRequired,
}
impl<
Loader: AssetLoader,
T: AssetTransformer<AssetInput = Loader::Asset>,
Saver: AssetSaver<Asset = T::AssetOutput>,
> Process for LoadTransformAndSave<Loader, T, Saver>
{
type Settings = LoadTransformAndSaveSettings<Loader::Settings, T::Settings, Saver::Settings>;
type OutputLoader = Saver::OutputLoader;
fn process<'a>(
&'a self,
context: &'a mut ProcessContext,
meta: AssetMeta<(), Self>,
writer: &'a mut Writer,
) -> BoxedFuture<'a, Result<<Self::OutputLoader as AssetLoader>::Settings, ProcessError>> {
Box::pin(async move {
let AssetAction::Process { settings, .. } = meta.asset else {
return Err(ProcessError::WrongMetaType);
};
let loader_meta = AssetMeta::<Loader, ()>::new(AssetAction::Load {
loader: std::any::type_name::<Loader>().to_string(),
settings: settings.loader_settings,
});
let loaded_asset = context
.load_source_asset(loader_meta)
.await?
.take::<Loader::Asset>()
.expect("Asset type is known");
let transformed_asset = self
.transformer
.transform(loaded_asset, &settings.transformer_settings)?;
let loaded_transformed_asset =
ErasedLoadedAsset::from(LoadedAsset::from(transformed_asset));
let saved_asset =
SavedAsset::<T::AssetOutput>::from_loaded(&loaded_transformed_asset).unwrap();
let output_settings = self
.saver
.save(writer, saved_asset, &settings.saver_settings)
.await
.map_err(|error| ProcessError::AssetSaveError(error.into()))?;
Ok(output_settings)
})
}
}
impl<Loader: AssetLoader, Saver: AssetSaver<Asset = Loader::Asset>> Process
for LoadAndSave<Loader, Saver>
{

View file

@ -0,0 +1,20 @@
use crate::{meta::Settings, Asset};
use serde::{Deserialize, Serialize};
/// Transforms an [`Asset`] of a given [`AssetTransformer::AssetInput`] type to an [`Asset`] of [`AssetTransformer::AssetOutput`] type.
pub trait AssetTransformer: Send + Sync + 'static {
/// The [`Asset`] type which this [`AssetTransformer`] takes as and input.
type AssetInput: Asset;
/// The [`Asset`] type which this [`AssetTransformer`] outputs.
type AssetOutput: Asset;
/// The settings type used by this [`AssetTransformer`].
type Settings: Settings + Default + Serialize + for<'a> Deserialize<'a>;
/// The type of [error](`std::error::Error`) which could be encountered by this saver.
type Error: Into<Box<dyn std::error::Error + Send + Sync + 'static>>;
fn transform<'a>(
&'a self,
asset: Self::AssetInput,
settings: &'a Self::Settings,
) -> Result<Self::AssetOutput, Box<dyn std::error::Error + Send + Sync + 'static>>;
}

View file

@ -1,12 +1,13 @@
//! This example illustrates how to define custom `AssetLoader`s and `AssetSaver`s, how to configure them, and how to register asset processors.
//! This example illustrates how to define custom `AssetLoader`s, `AssetTransfomers`, and `AssetSaver`s, how to configure them, and how to register asset processors.
use bevy::{
asset::{
embedded_asset,
io::{Reader, Writer},
processor::LoadAndSave,
processor::LoadTransformAndSave,
ron,
saver::{AssetSaver, SavedAsset},
transformer::AssetTransformer,
AssetLoader, AsyncReadExt, AsyncWriteExt, LoadContext,
},
prelude::*,
@ -14,6 +15,7 @@ use bevy::{
utils::{thiserror, BoxedFuture},
};
use serde::{Deserialize, Serialize};
use std::convert::Infallible;
use thiserror::Error;
fn main() {
@ -59,10 +61,10 @@ impl Plugin for TextPlugin {
.init_asset::<Text>()
.register_asset_loader(CoolTextLoader)
.register_asset_loader(TextLoader)
.register_asset_processor::<LoadAndSave<CoolTextLoader, CoolTextSaver>>(
LoadAndSave::from(CoolTextSaver),
.register_asset_processor::<LoadTransformAndSave<CoolTextLoader, CoolTextTransformer, CoolTextSaver>>(
LoadTransformAndSave::new(CoolTextTransformer, CoolTextSaver),
)
.set_default_asset_processor::<LoadAndSave<CoolTextLoader, CoolTextSaver>>("cool.ron");
.set_default_asset_processor::<LoadTransformAndSave<CoolTextLoader, CoolTextTransformer, CoolTextSaver>>("cool.ron");
}
}
@ -133,9 +135,7 @@ enum CoolTextLoaderError {
impl AssetLoader for CoolTextLoader {
type Asset = CoolText;
type Settings = ();
type Error = CoolTextLoaderError;
fn load<'a>(
@ -170,16 +170,37 @@ impl AssetLoader for CoolTextLoader {
}
}
struct CoolTextSaver;
#[derive(Default)]
struct CoolTextTransformer;
#[derive(Default, Serialize, Deserialize)]
pub struct CoolTextSaverSettings {
pub struct CoolTextTransformerSettings {
appended: String,
}
impl AssetTransformer for CoolTextTransformer {
type AssetInput = CoolText;
type AssetOutput = CoolText;
type Settings = CoolTextTransformerSettings;
type Error = Infallible;
fn transform<'a>(
&'a self,
asset: Self::AssetInput,
settings: &'a Self::Settings,
) -> Result<Self::AssetOutput, Box<dyn std::error::Error + Send + Sync + 'static>> {
Ok(CoolText {
text: format!("{}{}", asset.text, settings.appended),
dependencies: asset.dependencies.clone(),
})
}
}
struct CoolTextSaver;
impl AssetSaver for CoolTextSaver {
type Asset = CoolText;
type Settings = CoolTextSaverSettings;
type Settings = ();
type OutputLoader = TextLoader;
type Error = std::io::Error;
@ -187,11 +208,10 @@ impl AssetSaver for CoolTextSaver {
&'a self,
writer: &'a mut Writer,
asset: SavedAsset<'a, Self::Asset>,
settings: &'a Self::Settings,
_settings: &'a Self::Settings,
) -> BoxedFuture<'a, Result<TextSettings, Self::Error>> {
Box::pin(async move {
let text = format!("{}{}", asset.text.clone(), settings.appended);
writer.write_all(text.as_bytes()).await?;
writer.write_all(asset.text.as_bytes()).await?;
Ok(TextSettings::default())
})
}

View file

@ -1,12 +1,13 @@
(
meta_format_version: "1.0",
asset: Process(
processor: "bevy_asset::processor::process::LoadAndSave<asset_processing::CoolTextLoader, asset_processing::CoolTextSaver>",
processor: "bevy_asset::processor::process::LoadTransformAndSave<asset_processing::CoolTextLoader, asset_processing::CoolTextTransformer, asset_processing::CoolTextSaver>",
settings: (
loader_settings: (),
saver_settings: (
transformer_settings: (
appended: "X",
),
saver_settings: (),
),
),
)

View file

@ -3,6 +3,7 @@
dependencies: [
],
embedded_dependencies: [
"foo/c.cool.ron"
"foo/c.cool.ron",
"embedded://asset_processing/e.txt"
],
)

View file

@ -1,12 +1,13 @@
(
meta_format_version: "1.0",
asset: Process(
processor: "bevy_asset::processor::process::LoadAndSave<asset_processing::CoolTextLoader, asset_processing::CoolTextSaver>",
processor: "bevy_asset::processor::process::LoadTransformAndSave<asset_processing::CoolTextLoader, asset_processing::CoolTextTransformer, asset_processing::CoolTextSaver>",
settings: (
loader_settings: (),
saver_settings: (
transformer_settings: (
appended: "",
),
saver_settings: (),
),
),
)

View file

@ -1,12 +1,13 @@
(
meta_format_version: "1.0",
asset: Process(
processor: "bevy_asset::processor::process::LoadAndSave<asset_processing::CoolTextLoader, asset_processing::CoolTextSaver>",
processor: "bevy_asset::processor::process::LoadTransformAndSave<asset_processing::CoolTextLoader, asset_processing::CoolTextTransformer, asset_processing::CoolTextSaver>",
settings: (
loader_settings: (),
saver_settings: (
transformer_settings: (
appended: "",
),
saver_settings: (),
),
),
)

View file

@ -1,12 +1,13 @@
(
meta_format_version: "1.0",
asset: Process(
processor: "bevy_asset::processor::process::LoadAndSave<asset_processing::CoolTextLoader, asset_processing::CoolTextSaver>",
processor: "bevy_asset::processor::process::LoadTransformAndSave<asset_processing::CoolTextLoader, asset_processing::CoolTextTransformer, asset_processing::CoolTextSaver>",
settings: (
loader_settings: (),
saver_settings: (
transformer_settings: (
appended: "",
),
saver_settings: (),
),
),
)