bevy_asset/
loader.rs

1use crate::{
2    io::{AssetReaderError, MissingAssetSourceError, MissingProcessedAssetReaderError, Reader},
3    loader_builders::{Deferred, NestedLoader, StaticTyped},
4    meta::{AssetHash, AssetMeta, AssetMetaDyn, ProcessedInfoMinimal, Settings},
5    path::AssetPath,
6    Asset, AssetLoadError, AssetServer, AssetServerMode, Assets, Handle, UntypedAssetId,
7    UntypedHandle,
8};
9use alloc::{
10    boxed::Box,
11    string::{String, ToString},
12    vec::Vec,
13};
14use atomicow::CowArc;
15use bevy_ecs::{error::BevyError, world::World};
16use bevy_platform::collections::{HashMap, HashSet};
17use bevy_tasks::{BoxedFuture, ConditionalSendFuture};
18use core::any::{Any, TypeId};
19use downcast_rs::{impl_downcast, Downcast};
20use ron::error::SpannedError;
21use serde::{Deserialize, Serialize};
22use std::path::{Path, PathBuf};
23use thiserror::Error;
24
25/// Loads an [`Asset`] from a given byte [`Reader`]. This can accept [`AssetLoader::Settings`], which configure how the [`Asset`]
26/// should be loaded.
27///
28/// This trait is generally used in concert with [`AssetReader`](crate::io::AssetReader) to load assets from a byte source.
29///
30/// For a complementary version of this trait that can save assets, see [`AssetSaver`](crate::saver::AssetSaver).
31pub trait AssetLoader: Send + Sync + 'static {
32    /// The top level [`Asset`] loaded by this [`AssetLoader`].
33    type Asset: Asset;
34    /// The settings type used by this [`AssetLoader`].
35    type Settings: Settings + Default + Serialize + for<'a> Deserialize<'a>;
36    /// The type of [error](`std::error::Error`) which could be encountered by this loader.
37    type Error: Into<BevyError>;
38    /// Asynchronously loads [`AssetLoader::Asset`] (and any other labeled assets) from the bytes provided by [`Reader`].
39    fn load(
40        &self,
41        reader: &mut dyn Reader,
42        settings: &Self::Settings,
43        load_context: &mut LoadContext,
44    ) -> impl ConditionalSendFuture<Output = Result<Self::Asset, Self::Error>>;
45
46    /// Returns a list of extensions supported by this [`AssetLoader`], without the preceding dot.
47    /// Note that users of this [`AssetLoader`] may choose to load files with a non-matching extension.
48    fn extensions(&self) -> &[&str] {
49        &[]
50    }
51}
52
53/// Provides type-erased access to an [`AssetLoader`].
54pub trait ErasedAssetLoader: Send + Sync + 'static {
55    /// Asynchronously loads the asset(s) from the bytes provided by [`Reader`].
56    fn load<'a>(
57        &'a self,
58        reader: &'a mut dyn Reader,
59        meta: &'a dyn AssetMetaDyn,
60        load_context: LoadContext<'a>,
61    ) -> BoxedFuture<'a, Result<ErasedLoadedAsset, BevyError>>;
62
63    /// Returns a list of extensions supported by this asset loader, without the preceding dot.
64    fn extensions(&self) -> &[&str];
65    /// Deserializes metadata from the input `meta` bytes into the appropriate type (erased as [`Box<dyn AssetMetaDyn>`]).
66    fn deserialize_meta(&self, meta: &[u8]) -> Result<Box<dyn AssetMetaDyn>, DeserializeMetaError>;
67    /// Returns the default meta value for the [`AssetLoader`] (erased as [`Box<dyn AssetMetaDyn>`]).
68    fn default_meta(&self) -> Box<dyn AssetMetaDyn>;
69    /// Returns the type name of the [`AssetLoader`].
70    fn type_name(&self) -> &'static str;
71    /// Returns the [`TypeId`] of the [`AssetLoader`].
72    fn type_id(&self) -> TypeId;
73    /// Returns the type name of the top-level [`Asset`] loaded by the [`AssetLoader`].
74    fn asset_type_name(&self) -> &'static str;
75    /// Returns the [`TypeId`] of the top-level [`Asset`] loaded by the [`AssetLoader`].
76    fn asset_type_id(&self) -> TypeId;
77}
78
79impl<L> ErasedAssetLoader for L
80where
81    L: AssetLoader + Send + Sync,
82{
83    /// Processes the asset in an asynchronous closure.
84    fn load<'a>(
85        &'a self,
86        reader: &'a mut dyn Reader,
87        meta: &'a dyn AssetMetaDyn,
88        mut load_context: LoadContext<'a>,
89    ) -> BoxedFuture<'a, Result<ErasedLoadedAsset, BevyError>> {
90        Box::pin(async move {
91            let settings = meta
92                .loader_settings()
93                .expect("Loader settings should exist")
94                .downcast_ref::<L::Settings>()
95                .expect("AssetLoader settings should match the loader type");
96            let asset = <L as AssetLoader>::load(self, reader, settings, &mut load_context)
97                .await
98                .map_err(Into::into)?;
99            Ok(load_context.finish(asset).into())
100        })
101    }
102
103    fn extensions(&self) -> &[&str] {
104        <L as AssetLoader>::extensions(self)
105    }
106
107    fn deserialize_meta(&self, meta: &[u8]) -> Result<Box<dyn AssetMetaDyn>, DeserializeMetaError> {
108        let meta = AssetMeta::<L, ()>::deserialize(meta)?;
109        Ok(Box::new(meta))
110    }
111
112    fn default_meta(&self) -> Box<dyn AssetMetaDyn> {
113        Box::new(AssetMeta::<L, ()>::new(crate::meta::AssetAction::Load {
114            loader: self.type_name().to_string(),
115            settings: L::Settings::default(),
116        }))
117    }
118
119    fn type_name(&self) -> &'static str {
120        core::any::type_name::<L>()
121    }
122
123    fn type_id(&self) -> TypeId {
124        TypeId::of::<L>()
125    }
126
127    fn asset_type_name(&self) -> &'static str {
128        core::any::type_name::<L::Asset>()
129    }
130
131    fn asset_type_id(&self) -> TypeId {
132        TypeId::of::<L::Asset>()
133    }
134}
135
136pub(crate) struct LabeledAsset {
137    pub(crate) asset: ErasedLoadedAsset,
138    pub(crate) handle: UntypedHandle,
139}
140
141/// The successful result of an [`AssetLoader::load`] call. This contains the loaded "root" asset and any other "labeled" assets produced
142/// by the loader. It also holds the input [`AssetMeta`] (if it exists) and tracks dependencies:
143/// * normal dependencies: dependencies that must be loaded as part of this asset load (ex: assets a given asset has handles to).
144/// * Loader dependencies: dependencies whose actual asset values are used during the load process
145pub struct LoadedAsset<A: Asset> {
146    pub(crate) value: A,
147    pub(crate) dependencies: HashSet<UntypedAssetId>,
148    pub(crate) loader_dependencies: HashMap<AssetPath<'static>, AssetHash>,
149    pub(crate) labeled_assets: HashMap<CowArc<'static, str>, LabeledAsset>,
150}
151
152impl<A: Asset> LoadedAsset<A> {
153    /// Create a new loaded asset. This will use [`VisitAssetDependencies`](crate::VisitAssetDependencies) to populate `dependencies`.
154    pub fn new_with_dependencies(value: A) -> Self {
155        let mut dependencies = <HashSet<_>>::default();
156        value.visit_dependencies(&mut |id| {
157            dependencies.insert(id);
158        });
159        LoadedAsset {
160            value,
161            dependencies,
162            loader_dependencies: HashMap::default(),
163            labeled_assets: HashMap::default(),
164        }
165    }
166
167    /// Cast (and take ownership) of the [`Asset`] value of the given type.
168    pub fn take(self) -> A {
169        self.value
170    }
171
172    /// Retrieves a reference to the internal [`Asset`] type.
173    pub fn get(&self) -> &A {
174        &self.value
175    }
176
177    /// Returns the [`ErasedLoadedAsset`] for the given label, if it exists.
178    pub fn get_labeled(
179        &self,
180        label: impl Into<CowArc<'static, str>>,
181    ) -> Option<&ErasedLoadedAsset> {
182        self.labeled_assets.get(&label.into()).map(|a| &a.asset)
183    }
184
185    /// Iterate over all labels for "labeled assets" in the loaded asset
186    pub fn iter_labels(&self) -> impl Iterator<Item = &str> {
187        self.labeled_assets.keys().map(|s| &**s)
188    }
189}
190
191impl<A: Asset> From<A> for LoadedAsset<A> {
192    fn from(asset: A) -> Self {
193        LoadedAsset::new_with_dependencies(asset)
194    }
195}
196
197/// A "type erased / boxed" counterpart to [`LoadedAsset`]. This is used in places where the loaded type is not statically known.
198pub struct ErasedLoadedAsset {
199    pub(crate) value: Box<dyn AssetContainer>,
200    pub(crate) dependencies: HashSet<UntypedAssetId>,
201    pub(crate) loader_dependencies: HashMap<AssetPath<'static>, AssetHash>,
202    pub(crate) labeled_assets: HashMap<CowArc<'static, str>, LabeledAsset>,
203}
204
205impl<A: Asset> From<LoadedAsset<A>> for ErasedLoadedAsset {
206    fn from(asset: LoadedAsset<A>) -> Self {
207        ErasedLoadedAsset {
208            value: Box::new(asset.value),
209            dependencies: asset.dependencies,
210            loader_dependencies: asset.loader_dependencies,
211            labeled_assets: asset.labeled_assets,
212        }
213    }
214}
215
216impl ErasedLoadedAsset {
217    /// Cast (and take ownership) of the [`Asset`] value of the given type. This will return [`Some`] if
218    /// the stored type matches `A` and [`None`] if it does not.
219    pub fn take<A: Asset>(self) -> Option<A> {
220        self.value.downcast::<A>().map(|a| *a).ok()
221    }
222
223    /// Retrieves a reference to the internal [`Asset`] type, if it matches the type `A`. Otherwise returns [`None`].
224    pub fn get<A: Asset>(&self) -> Option<&A> {
225        self.value.downcast_ref::<A>()
226    }
227
228    /// Retrieves the [`TypeId`] of the stored [`Asset`] type.
229    pub fn asset_type_id(&self) -> TypeId {
230        (*self.value).type_id()
231    }
232
233    /// Retrieves the `type_name` of the stored [`Asset`] type.
234    pub fn asset_type_name(&self) -> &'static str {
235        self.value.asset_type_name()
236    }
237
238    /// Returns the [`ErasedLoadedAsset`] for the given label, if it exists.
239    pub fn get_labeled(
240        &self,
241        label: impl Into<CowArc<'static, str>>,
242    ) -> Option<&ErasedLoadedAsset> {
243        self.labeled_assets.get(&label.into()).map(|a| &a.asset)
244    }
245
246    /// Iterate over all labels for "labeled assets" in the loaded asset
247    pub fn iter_labels(&self) -> impl Iterator<Item = &str> {
248        self.labeled_assets.keys().map(|s| &**s)
249    }
250
251    /// Cast this loaded asset as the given type. If the type does not match,
252    /// the original type-erased asset is returned.
253    pub fn downcast<A: Asset>(mut self) -> Result<LoadedAsset<A>, ErasedLoadedAsset> {
254        match self.value.downcast::<A>() {
255            Ok(value) => Ok(LoadedAsset {
256                value: *value,
257                dependencies: self.dependencies,
258                loader_dependencies: self.loader_dependencies,
259                labeled_assets: self.labeled_assets,
260            }),
261            Err(value) => {
262                self.value = value;
263                Err(self)
264            }
265        }
266    }
267}
268
269/// A type erased container for an [`Asset`] value that is capable of inserting the [`Asset`] into a [`World`]'s [`Assets`] collection.
270pub trait AssetContainer: Downcast + Any + Send + Sync + 'static {
271    fn insert(self: Box<Self>, id: UntypedAssetId, world: &mut World);
272    fn asset_type_name(&self) -> &'static str;
273}
274
275impl_downcast!(AssetContainer);
276
277impl<A: Asset> AssetContainer for A {
278    fn insert(self: Box<Self>, id: UntypedAssetId, world: &mut World) {
279        // We only ever call this if we know the asset is still alive, so it is fine to unwrap here.
280        world
281            .resource_mut::<Assets<A>>()
282            .insert(id.typed(), *self)
283            .expect("the AssetId is still valid");
284    }
285
286    fn asset_type_name(&self) -> &'static str {
287        core::any::type_name::<A>()
288    }
289}
290
291/// An error that occurs when attempting to call [`NestedLoader::load`] which
292/// is configured to work [immediately].
293///
294/// [`NestedLoader::load`]: crate::NestedLoader::load
295/// [immediately]: crate::Immediate
296#[derive(Error, Debug)]
297pub enum LoadDirectError {
298    #[error("Requested to load an asset path ({0:?}) with a subasset, but this is unsupported. See issue #18291")]
299    RequestedSubasset(AssetPath<'static>),
300    #[error("Failed to load dependency {dependency:?} {error}")]
301    LoadError {
302        dependency: AssetPath<'static>,
303        error: AssetLoadError,
304    },
305}
306
307/// An error that occurs while deserializing [`AssetMeta`].
308#[derive(Error, Debug, Clone, PartialEq, Eq)]
309pub enum DeserializeMetaError {
310    #[error("Failed to deserialize asset meta: {0:?}")]
311    DeserializeSettings(#[from] SpannedError),
312    #[error("Failed to deserialize minimal asset meta: {0:?}")]
313    DeserializeMinimal(SpannedError),
314}
315
316/// A context that provides access to assets in [`AssetLoader`]s, tracks dependencies, and collects asset load state.
317///
318/// Any asset state accessed by [`LoadContext`] will be tracked and stored for use in dependency events and asset preprocessing.
319pub struct LoadContext<'a> {
320    pub(crate) asset_server: &'a AssetServer,
321    pub(crate) should_load_dependencies: bool,
322    populate_hashes: bool,
323    asset_path: AssetPath<'static>,
324    pub(crate) dependencies: HashSet<UntypedAssetId>,
325    /// Direct dependencies used by this loader.
326    pub(crate) loader_dependencies: HashMap<AssetPath<'static>, AssetHash>,
327    pub(crate) labeled_assets: HashMap<CowArc<'static, str>, LabeledAsset>,
328}
329
330impl<'a> LoadContext<'a> {
331    /// Creates a new [`LoadContext`] instance.
332    pub(crate) fn new(
333        asset_server: &'a AssetServer,
334        asset_path: AssetPath<'static>,
335        should_load_dependencies: bool,
336        populate_hashes: bool,
337    ) -> Self {
338        Self {
339            asset_server,
340            asset_path,
341            populate_hashes,
342            should_load_dependencies,
343            dependencies: HashSet::default(),
344            loader_dependencies: HashMap::default(),
345            labeled_assets: HashMap::default(),
346        }
347    }
348
349    /// Begins a new labeled asset load. Use the returned [`LoadContext`] to load
350    /// dependencies for the new asset and call [`LoadContext::finish`] to finalize the asset load.
351    /// When finished, make sure you call [`LoadContext::add_loaded_labeled_asset`] to add the results back to the parent
352    /// context.
353    /// Prefer [`LoadContext::labeled_asset_scope`] when possible, which will automatically add
354    /// the labeled [`LoadContext`] back to the parent context.
355    /// [`LoadContext::begin_labeled_asset`] exists largely to enable parallel asset loading.
356    ///
357    /// See [`AssetPath`] for more on labeled assets.
358    ///
359    /// ```no_run
360    /// # use bevy_asset::{Asset, LoadContext};
361    /// # use bevy_reflect::TypePath;
362    /// # #[derive(Asset, TypePath, Default)]
363    /// # struct Image;
364    /// # let load_context: LoadContext = panic!();
365    /// let mut handles = Vec::new();
366    /// for i in 0..2 {
367    ///     let labeled = load_context.begin_labeled_asset();
368    ///     handles.push(std::thread::spawn(move || {
369    ///         (i.to_string(), labeled.finish(Image::default()))
370    ///     }));
371    /// }
372    ///
373    /// for handle in handles {
374    ///     let (label, loaded_asset) = handle.join().unwrap();
375    ///     load_context.add_loaded_labeled_asset(label, loaded_asset);
376    /// }
377    /// ```
378    pub fn begin_labeled_asset(&self) -> LoadContext<'_> {
379        LoadContext::new(
380            self.asset_server,
381            self.asset_path.clone(),
382            self.should_load_dependencies,
383            self.populate_hashes,
384        )
385    }
386
387    /// Creates a new [`LoadContext`] for the given `label`. The `load` function is responsible for loading an [`Asset`] of
388    /// type `A`. `load` will be called immediately and the result will be used to finalize the [`LoadContext`], resulting in a new
389    /// [`LoadedAsset`], which is registered under the `label` label.
390    ///
391    /// This exists to remove the need to manually call [`LoadContext::begin_labeled_asset`] and then manually register the
392    /// result with [`LoadContext::add_loaded_labeled_asset`].
393    ///
394    /// See [`AssetPath`] for more on labeled assets.
395    pub fn labeled_asset_scope<A: Asset, E>(
396        &mut self,
397        label: String,
398        load: impl FnOnce(&mut LoadContext) -> Result<A, E>,
399    ) -> Result<Handle<A>, E> {
400        let mut context = self.begin_labeled_asset();
401        let asset = load(&mut context)?;
402        let loaded_asset = context.finish(asset);
403        Ok(self.add_loaded_labeled_asset(label, loaded_asset))
404    }
405
406    /// This will add the given `asset` as a "labeled [`Asset`]" with the `label` label.
407    ///
408    /// # Warning
409    ///
410    /// This will not assign dependencies to the given `asset`. If adding an asset
411    /// with dependencies generated from calls such as [`LoadContext::load`], use
412    /// [`LoadContext::labeled_asset_scope`] or [`LoadContext::begin_labeled_asset`] to generate a
413    /// new [`LoadContext`] to track the dependencies for the labeled asset.
414    ///
415    /// See [`AssetPath`] for more on labeled assets.
416    pub fn add_labeled_asset<A: Asset>(&mut self, label: String, asset: A) -> Handle<A> {
417        self.labeled_asset_scope(label, |_| Ok::<_, ()>(asset))
418            .expect("the closure returns Ok")
419    }
420
421    /// Add a [`LoadedAsset`] that is a "labeled sub asset" of the root path of this load context.
422    /// This can be used in combination with [`LoadContext::begin_labeled_asset`] to parallelize
423    /// sub asset loading.
424    ///
425    /// See [`AssetPath`] for more on labeled assets.
426    pub fn add_loaded_labeled_asset<A: Asset>(
427        &mut self,
428        label: impl Into<CowArc<'static, str>>,
429        loaded_asset: LoadedAsset<A>,
430    ) -> Handle<A> {
431        let label = label.into();
432        let loaded_asset: ErasedLoadedAsset = loaded_asset.into();
433        let labeled_path = self.asset_path.clone().with_label(label.clone());
434        let handle = self
435            .asset_server
436            .get_or_create_path_handle(labeled_path, None);
437        self.labeled_assets.insert(
438            label,
439            LabeledAsset {
440                asset: loaded_asset,
441                handle: handle.clone().untyped(),
442            },
443        );
444        handle
445    }
446
447    /// Returns `true` if an asset with the label `label` exists in this context.
448    ///
449    /// See [`AssetPath`] for more on labeled assets.
450    pub fn has_labeled_asset<'b>(&self, label: impl Into<CowArc<'b, str>>) -> bool {
451        let path = self.asset_path.clone().with_label(label.into());
452        !self.asset_server.get_handles_untyped(&path).is_empty()
453    }
454
455    /// "Finishes" this context by populating the final [`Asset`] value.
456    pub fn finish<A: Asset>(self, value: A) -> LoadedAsset<A> {
457        LoadedAsset {
458            value,
459            dependencies: self.dependencies,
460            loader_dependencies: self.loader_dependencies,
461            labeled_assets: self.labeled_assets,
462        }
463    }
464
465    /// Gets the source path for this load context.
466    pub fn path(&self) -> &Path {
467        self.asset_path.path()
468    }
469
470    /// Gets the source asset path for this load context.
471    pub fn asset_path(&self) -> &AssetPath<'static> {
472        &self.asset_path
473    }
474
475    /// Reads the asset at the given path and returns its bytes
476    pub async fn read_asset_bytes<'b, 'c>(
477        &'b mut self,
478        path: impl Into<AssetPath<'c>>,
479    ) -> Result<Vec<u8>, ReadAssetBytesError> {
480        let path = path.into();
481        let source = self.asset_server.get_source(path.source())?;
482        let asset_reader = match self.asset_server.mode() {
483            AssetServerMode::Unprocessed => source.reader(),
484            AssetServerMode::Processed => source.processed_reader()?,
485        };
486        let mut reader = asset_reader.read(path.path()).await?;
487        let hash = if self.populate_hashes {
488            // NOTE: ensure meta is read while the asset bytes reader is still active to ensure transactionality
489            // See `ProcessorGatedReader` for more info
490            let meta_bytes = asset_reader.read_meta_bytes(path.path()).await?;
491            let minimal: ProcessedInfoMinimal = ron::de::from_bytes(&meta_bytes)
492                .map_err(DeserializeMetaError::DeserializeMinimal)?;
493            let processed_info = minimal
494                .processed_info
495                .ok_or(ReadAssetBytesError::MissingAssetHash)?;
496            processed_info.full_hash
497        } else {
498            Default::default()
499        };
500        let mut bytes = Vec::new();
501        reader
502            .read_to_end(&mut bytes)
503            .await
504            .map_err(|source| ReadAssetBytesError::Io {
505                path: path.path().to_path_buf(),
506                source,
507            })?;
508        self.loader_dependencies.insert(path.clone_owned(), hash);
509        Ok(bytes)
510    }
511
512    /// Returns a handle to an asset of type `A` with the label `label`. This [`LoadContext`] must produce an asset of the
513    /// given type and the given label or the dependencies of this asset will never be considered "fully loaded". However you
514    /// can call this method before _or_ after adding the labeled asset.
515    pub fn get_label_handle<'b, A: Asset>(
516        &mut self,
517        label: impl Into<CowArc<'b, str>>,
518    ) -> Handle<A> {
519        let path = self.asset_path.clone().with_label(label);
520        let handle = self.asset_server.get_or_create_path_handle::<A>(path, None);
521        self.dependencies.insert(handle.id().untyped());
522        handle
523    }
524
525    pub(crate) async fn load_direct_internal(
526        &mut self,
527        path: AssetPath<'static>,
528        meta: &dyn AssetMetaDyn,
529        loader: &dyn ErasedAssetLoader,
530        reader: &mut dyn Reader,
531    ) -> Result<ErasedLoadedAsset, LoadDirectError> {
532        let loaded_asset = self
533            .asset_server
534            .load_with_meta_loader_and_reader(
535                &path,
536                meta,
537                loader,
538                reader,
539                false,
540                self.populate_hashes,
541            )
542            .await
543            .map_err(|error| LoadDirectError::LoadError {
544                dependency: path.clone(),
545                error,
546            })?;
547        let info = meta.processed_info().as_ref();
548        let hash = info.map(|i| i.full_hash).unwrap_or_default();
549        self.loader_dependencies.insert(path, hash);
550        Ok(loaded_asset)
551    }
552
553    /// Create a builder for loading nested assets in this context.
554    #[must_use]
555    pub fn loader(&mut self) -> NestedLoader<'a, '_, StaticTyped, Deferred> {
556        NestedLoader::new(self)
557    }
558
559    /// Retrieves a handle for the asset at the given path and adds that path as a dependency of the asset.
560    /// If the current context is a normal [`AssetServer::load`], an actual asset load will be kicked off immediately, which ensures the load happens
561    /// as soon as possible.
562    /// "Normal loads" kicked from within a normal Bevy App will generally configure the context to kick off loads immediately.
563    /// If the current context is configured to not load dependencies automatically (ex: [`AssetProcessor`](crate::processor::AssetProcessor)),
564    /// a load will not be kicked off automatically. It is then the calling context's responsibility to begin a load if necessary.
565    ///
566    /// If you need to override asset settings, asset type, or load directly, please see [`LoadContext::loader`].
567    pub fn load<'b, A: Asset>(&mut self, path: impl Into<AssetPath<'b>>) -> Handle<A> {
568        self.loader().load(path)
569    }
570}
571
572/// An error produced when calling [`LoadContext::read_asset_bytes`]
573#[derive(Error, Debug)]
574pub enum ReadAssetBytesError {
575    #[error(transparent)]
576    DeserializeMetaError(#[from] DeserializeMetaError),
577    #[error(transparent)]
578    AssetReaderError(#[from] AssetReaderError),
579    #[error(transparent)]
580    MissingAssetSourceError(#[from] MissingAssetSourceError),
581    #[error(transparent)]
582    MissingProcessedAssetReaderError(#[from] MissingProcessedAssetReaderError),
583    /// Encountered an I/O error while loading an asset.
584    #[error("Encountered an io error while loading asset at `{}`: {source}", path.display())]
585    Io {
586        path: PathBuf,
587        source: std::io::Error,
588    },
589    #[error("The LoadContext for this read_asset_bytes call requires hash metadata, but it was not provided. This is likely an internal implementation error.")]
590    MissingAssetHash,
591}