From 284e61c9d8953902731012f50bd810437d8702e9 Mon Sep 17 00:00:00 2001 From: Brian Reavis Date: Tue, 2 Dec 2025 16:12:58 -0800 Subject: [PATCH 1/6] Configurable asset storage --- crates/bevy_asset/macros/src/lib.rs | 19 +- crates/bevy_asset/src/asset_changed.rs | 2 +- crates/bevy_asset/src/assets.rs | 198 ++++++++--- crates/bevy_asset/src/handle.rs | 6 +- crates/bevy_asset/src/lib.rs | 16 +- crates/bevy_asset/src/meta.rs | 6 +- crates/bevy_asset/src/reflect.rs | 132 ++++++-- crates/bevy_asset/src/storage/arced.rs | 77 +++++ crates/bevy_asset/src/storage/boxed.rs | 72 ++++ crates/bevy_asset/src/storage/hybrid.rs | 318 ++++++++++++++++++ crates/bevy_asset/src/storage/mod.rs | 78 +++++ crates/bevy_asset/src/storage/stack.rs | 123 +++++++ crates/bevy_gizmos_render/src/lib.rs | 5 +- .../bevy_image/src/texture_atlas_builder.rs | 7 +- crates/bevy_pbr/src/light_probe/generate.rs | 35 +- crates/bevy_pbr/src/material.rs | 13 +- crates/bevy_pbr/src/medium.rs | 7 +- crates/bevy_pbr/src/render/skin.rs | 8 +- crates/bevy_pbr/src/wireframe.rs | 6 +- .../src/auto_exposure/compensation_curve.rs | 5 +- crates/bevy_render/src/erased_render_asset.rs | 26 +- crates/bevy_render/src/mesh/mod.rs | 6 +- crates/bevy_render/src/render_asset.rs | 26 +- crates/bevy_render/src/storage.rs | 10 +- crates/bevy_render/src/texture/gpu_image.rs | 21 +- .../bevy_sprite_render/src/mesh2d/material.rs | 13 +- .../src/mesh2d/wireframe2d.rs | 6 +- crates/bevy_ui_render/src/ui_material.rs | 6 +- .../src/ui_material_pipeline.rs | 4 +- examples/asset/asset_decompression.rs | 10 +- examples/shader_advanced/manual_material.rs | 5 +- 31 files changed, 1081 insertions(+), 185 deletions(-) create mode 100644 crates/bevy_asset/src/storage/arced.rs create mode 100644 crates/bevy_asset/src/storage/boxed.rs create mode 100644 crates/bevy_asset/src/storage/hybrid.rs create mode 100644 crates/bevy_asset/src/storage/mod.rs create mode 100644 crates/bevy_asset/src/storage/stack.rs diff --git a/crates/bevy_asset/macros/src/lib.rs b/crates/bevy_asset/macros/src/lib.rs index 12268399ecf00..08833a50f03dd 100644 --- a/crates/bevy_asset/macros/src/lib.rs +++ b/crates/bevy_asset/macros/src/lib.rs @@ -12,9 +12,10 @@ pub(crate) fn bevy_asset_path() -> Path { } const DEPENDENCY_ATTRIBUTE: &str = "dependency"; +const ASSET_STORAGE_ATTRIBUTE: &str = "asset_storage"; /// Implement the `Asset` trait. -#[proc_macro_derive(Asset, attributes(dependency))] +#[proc_macro_derive(Asset, attributes(dependency, asset_storage))] pub fn derive_asset(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); let bevy_asset_path: Path = bevy_asset_path(); @@ -26,8 +27,22 @@ pub fn derive_asset(input: TokenStream) -> TokenStream { Err(err) => return err.into_compile_error().into(), }; + // Check for custom asset_storage attribute + let storage_type = ast + .attrs + .iter() + .find(|attr| attr.path().is_ident(ASSET_STORAGE_ATTRIBUTE)) + .and_then(|attr| attr.parse_args::().ok()) + .unwrap_or_else(|| { + // Default to StackAssetStorage if no custom storage is specified + let raw_storage = format_ident!("StackAssetStorage"); + syn::parse_quote!(#bevy_asset_path::#raw_storage) + }); + TokenStream::from(quote! { - impl #impl_generics #bevy_asset_path::Asset for #struct_name #type_generics #where_clause { } + impl #impl_generics #bevy_asset_path::Asset for #struct_name #type_generics #where_clause { + type AssetStorage = #storage_type; + } #dependency_visitor }) } diff --git a/crates/bevy_asset/src/asset_changed.rs b/crates/bevy_asset/src/asset_changed.rs index fdf8b53f8d622..d3c8c05df81d9 100644 --- a/crates/bevy_asset/src/asset_changed.rs +++ b/crates/bevy_asset/src/asset_changed.rs @@ -361,7 +361,7 @@ mod tests { .iter() .find_map(|(h, a)| (a.0 == i).then_some(h)) .unwrap(); - let asset = assets.get_mut(id).unwrap(); + let mut asset = assets.get_mut(id).unwrap(); println!("setting new value for {}", asset.0); asset.1 = "new_value"; }; diff --git a/crates/bevy_asset/src/assets.rs b/crates/bevy_asset/src/assets.rs index c315ca05e35e4..e210bd8fe3540 100644 --- a/crates/bevy_asset/src/assets.rs +++ b/crates/bevy_asset/src/assets.rs @@ -14,6 +14,11 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use uuid::Uuid; +use crate::storage::{ + AssetMut, AssetRef, AssetSnapshot, AssetSnapshotStrategy, AssetStorageStrategy, + AssetWriteStrategy, StoredAsset, +}; + /// A generational runtime-only identifier for a specific [`Asset`] stored in [`Assets`]. This is optimized for efficient runtime /// usage and is not suitable for identifying assets across app runs. #[derive( @@ -107,7 +112,10 @@ enum Entry { #[default] None, /// Some is an indicator that there is a live handle active for the entry at this [`AssetIndex`] - Some { value: Option, generation: u32 }, + Some { + value: Option>, + generation: u32, + }, } /// Stores [`Asset`] values in a Vec-like storage identified by [`AssetIndex`]. @@ -152,7 +160,7 @@ impl DenseAssetStorage { if !exists { self.len += 1; } - *value = Some(asset); + *value = Some(A::AssetStorage::new(asset)); Ok(exists) } else { Err(InvalidGenerationError::Occupied { @@ -167,7 +175,7 @@ impl DenseAssetStorage { /// Removes the asset stored at the given `index` and returns it as [`Some`] (if the asset exists). /// This will recycle the id and allow new entries to be inserted. - pub(crate) fn remove_dropped(&mut self, index: AssetIndex) -> Option { + pub(crate) fn remove_dropped(&mut self, index: AssetIndex) -> Option> { self.remove_internal(index, |dense_storage| { dense_storage.storage[index.index as usize] = Entry::None; dense_storage.allocator.recycle(index); @@ -177,7 +185,7 @@ impl DenseAssetStorage { /// Removes the asset stored at the given `index` and returns it as [`Some`] (if the asset exists). /// This will _not_ recycle the id. New values with the current ID can still be inserted. The ID will /// not be reused until [`DenseAssetStorage::remove_dropped`] is called. - pub(crate) fn remove_still_alive(&mut self, index: AssetIndex) -> Option { + pub(crate) fn remove_still_alive(&mut self, index: AssetIndex) -> Option> { self.remove_internal(index, |_| {}) } @@ -185,7 +193,7 @@ impl DenseAssetStorage { &mut self, index: AssetIndex, removed_action: impl FnOnce(&mut Self), - ) -> Option { + ) -> Option> { self.flush(); let value = match &mut self.storage[index.index as usize] { Entry::None => return None, @@ -201,7 +209,7 @@ impl DenseAssetStorage { value } - pub(crate) fn get(&self, index: AssetIndex) -> Option<&A> { + pub(crate) fn get(&self, index: AssetIndex) -> Option<&StoredAsset> { let entry = self.storage.get(index.index as usize)?; match entry { Entry::None => None, @@ -215,7 +223,7 @@ impl DenseAssetStorage { } } - pub(crate) fn get_mut(&mut self, index: AssetIndex) -> Option<&mut A> { + pub(crate) fn get_mut(&mut self, index: AssetIndex) -> Option<&mut StoredAsset> { let entry = self.storage.get_mut(index.index as usize)?; match entry { Entry::None => None, @@ -272,6 +280,42 @@ impl DenseAssetStorage { } } +pub struct StoredAssetEntry<'a, A: Asset> { + stored_asset: &'a mut StoredAsset, +} + +impl<'a, A: Asset> StoredAssetEntry<'a, A> { + pub fn as_ref(&'a self) -> AssetRef<'a, A> { + A::AssetStorage::get_ref(self.stored_asset) + } +} + +impl<'a, A: Asset> StoredAssetEntry<'a, A> +where + A::AssetStorage: AssetWriteStrategy, +{ + pub fn as_mut(&'a mut self) -> AssetMut<'a, A> { + A::AssetStorage::get_mut(self.stored_asset) + } +} + +impl<'a, A: Asset> StoredAssetEntry<'a, A> +where + A::AssetStorage: AssetSnapshotStrategy, +{ + /// Returns a snapshot of the asset, which is a clone of the asset `A` (or an `Arc` clone, depending on the storage strategy). + pub fn snapshot(&'a mut self) -> AssetSnapshot { + A::AssetStorage::get_snapshot(self.stored_asset) + } + /// Instead of returning a clone of the asset or an Arc clone like [`StoredAssetEntry::snapshot`], + /// this will take ownership of the asset and put the entry in [`Assets`] into an erased state. + /// + /// Future attempts to get the asset will fail. + pub fn snapshot_erased(&'a mut self) -> AssetSnapshot { + A::AssetStorage::get_snapshot_erased(self.stored_asset) + } +} + /// Stores [`Asset`] values identified by their [`AssetId`]. /// /// Assets identified by [`AssetId::Index`] will be stored in a "dense" vec-like storage. This is more efficient, but it means that @@ -286,7 +330,7 @@ impl DenseAssetStorage { #[derive(Resource)] pub struct Assets { dense_storage: DenseAssetStorage, - hash_map: HashMap, + hash_map: HashMap>, handle_provider: AssetHandleProvider, queued_events: Vec>, /// Assets managed by the `Assets` struct with live strong `Handle`s @@ -339,26 +383,27 @@ impl Assets { } } - /// Retrieves an [`Asset`] stored for the given `id` if it exists. If it does not exist, it will - /// be inserted using `insert_fn`. - /// - /// Note: This will never return an error for UUID asset IDs. - // PERF: Optimize this or remove it - pub fn get_or_insert_with( - &mut self, - id: impl Into>, - insert_fn: impl FnOnce() -> A, - ) -> Result<&mut A, InvalidGenerationError> { - let id: AssetId = id.into(); - if self.get(id).is_none() { - self.insert(id, insert_fn())?; - } - // This should be impossible since either, `self.get` was Some, in which case this succeeds, - // or `self.get` was None and we inserted it (and bailed out if there was an error). - Ok(self - .get_mut(id) - .expect("the Asset was none even though we checked or inserted")) - } + // /// Retrieves an [`Asset`] stored for the given `id` if it exists. If it does not exist, it will + // /// be inserted using `insert_fn`. + // /// + // /// Note: This will never return an error for UUID asset IDs. + // // PERF: Optimize this or remove it + // #[deprecated] + // pub fn get_or_insert_with( + // &mut self, + // id: impl Into>, + // insert_fn: impl FnOnce() -> A, + // ) -> Result, InvalidGenerationError> { + // let id: AssetId = id.into(); + // if self.get(id).is_none() { + // self.insert(id, insert_fn())?; + // } + // // This should be impossible since either, `self.get` was Some, in which case this succeeds, + // // or `self.get` was None and we inserted it (and bailed out if there was an error). + // Ok(self + // .get_mut(id) + // .expect("the Asset was none even though we checked or inserted")) + // } /// Returns `true` if the `id` exists in this collection. Otherwise it returns `false`. pub fn contains(&self, id: impl Into>) -> bool { @@ -368,8 +413,9 @@ impl Assets { } } - pub(crate) fn insert_with_uuid(&mut self, uuid: Uuid, asset: A) -> Option { - let result = self.hash_map.insert(uuid, asset); + pub(crate) fn insert_with_uuid(&mut self, uuid: Uuid, asset: A) -> Option> { + let stored_asset = A::AssetStorage::new(asset); + let result = self.hash_map.insert(uuid, stored_asset); if result.is_some() { self.queued_events .push(AssetEvent::Modified { id: uuid.into() }); @@ -426,43 +472,90 @@ impl Assets { /// Retrieves a reference to the [`Asset`] with the given `id`, if it exists. /// Note that this supports anything that implements `Into>`, which includes [`Handle`] and [`AssetId`]. #[inline] - pub fn get(&self, id: impl Into>) -> Option<&A> { - match id.into() { + pub fn entry(&mut self, id: impl Into>) -> Option> { + let stored_asset = match id.into() { + AssetId::Index { index, .. } => self.dense_storage.get_mut(index), + AssetId::Uuid { uuid } => self.hash_map.get_mut(&uuid), + }; + stored_asset.map(|stored_asset| StoredAssetEntry { stored_asset }) + } + + /// Retrieves a reference to the [`Asset`] with the given `id`, if it exists. + /// Note that this supports anything that implements `Into>`, which includes [`Handle`] and [`AssetId`]. + #[inline] + pub fn get(&self, id: impl Into>) -> Option> { + let stored_asset = match id.into() { AssetId::Index { index, .. } => self.dense_storage.get(index), AssetId::Uuid { uuid } => self.hash_map.get(&uuid), - } + }; + stored_asset.map(|stored_asset| A::AssetStorage::get_ref(stored_asset)) + } + + /// Returns a snapshot of the [`Asset`] with the given `id`, if it exists. For sometimes, this will be + /// a clone (memory copy), but if's a asset using [`ArcedStorageStrategy`], it will be a cheap arc clone. + #[inline] + pub fn get_snapshot(&mut self, id: impl Into>) -> Option> + where + A::AssetStorage: AssetSnapshotStrategy, + { + let stored_asset = match id.into() { + AssetId::Index { index, .. } => self.dense_storage.get_mut(index), + AssetId::Uuid { uuid } => self.hash_map.get_mut(&uuid), + }; + stored_asset.map(|stored_asset| A::AssetStorage::get_snapshot(stored_asset)) + } + + /// Returns a snapshot of the [`Asset`] with the given `id`, if it exists. For sometimes, this will be + /// a clone (memory copy), but if's a asset using [`ArcedStorageStrategy`], it will be a cheap arc clone. + #[inline] + pub fn get_snapshot_erased(&mut self, id: impl Into>) -> Option> + where + A::AssetStorage: AssetSnapshotStrategy, + { + let stored_asset = match id.into() { + AssetId::Index { index, .. } => self.dense_storage.get_mut(index), + AssetId::Uuid { uuid } => self.hash_map.get_mut(&uuid), + }; + stored_asset.map(|stored_asset| A::AssetStorage::get_snapshot_erased(stored_asset)) } /// Retrieves a mutable reference to the [`Asset`] with the given `id`, if it exists. /// Note that this supports anything that implements `Into>`, which includes [`Handle`] and [`AssetId`]. #[inline] - pub fn get_mut(&mut self, id: impl Into>) -> Option<&mut A> { + pub fn get_mut<'a>(&'a mut self, id: impl Into>) -> Option> + where + A::AssetStorage: AssetWriteStrategy, + { let id: AssetId = id.into(); - let result = match id { + let stored_asset = match id { AssetId::Index { index, .. } => self.dense_storage.get_mut(index), AssetId::Uuid { uuid } => self.hash_map.get_mut(&uuid), }; - if result.is_some() { + if stored_asset.is_some() { self.queued_events.push(AssetEvent::Modified { id }); } - result + stored_asset.map(|stored_asset| A::AssetStorage::get_mut(stored_asset)) } /// Retrieves a mutable reference to the [`Asset`] with the given `id`, if it exists. /// /// This is the same as [`Assets::get_mut`] except it doesn't emit [`AssetEvent::Modified`]. #[inline] - pub fn get_mut_untracked(&mut self, id: impl Into>) -> Option<&mut A> { + pub fn get_mut_untracked<'a>(&'a mut self, id: impl Into>) -> Option> + where + A::AssetStorage: AssetWriteStrategy, + { let id: AssetId = id.into(); - match id { + let stored_asset = match id { AssetId::Index { index, .. } => self.dense_storage.get_mut(index), AssetId::Uuid { uuid } => self.hash_map.get_mut(&uuid), - } + }; + stored_asset.map(|stored_asset| A::AssetStorage::get_mut(stored_asset)) } /// Removes (and returns) the [`Asset`] with the given `id`, if it exists. /// Note that this supports anything that implements `Into>`, which includes [`Handle`] and [`AssetId`]. - pub fn remove(&mut self, id: impl Into>) -> Option { + pub fn remove(&mut self, id: impl Into>) -> Option> { let id: AssetId = id.into(); let result = self.remove_untracked(id); if result.is_some() { @@ -475,7 +568,7 @@ impl Assets { /// Note that this supports anything that implements `Into>`, which includes [`Handle`] and [`AssetId`]. /// /// This is the same as [`Assets::remove`] except it doesn't emit [`AssetEvent::Removed`]. - pub fn remove_untracked(&mut self, id: impl Into>) -> Option { + pub fn remove_untracked(&mut self, id: impl Into>) -> Option> { let id: AssetId = id.into(); match id { AssetId::Index { index, .. } => { @@ -528,14 +621,14 @@ impl Assets { /// Returns an iterator over the [`AssetId`] and [`Asset`] ref of every asset in this collection. // PERF: this could be accelerated if we implement a skip list. Consider the cost/benefits - pub fn iter(&self) -> impl Iterator, &A)> { + pub fn iter(&self) -> impl Iterator, AssetRef<'_, A>)> + '_ { self.dense_storage .storage .iter() .enumerate() .filter_map(|(i, v)| match v { Entry::None => None, - Entry::Some { value, generation } => value.as_ref().map(|v| { + Entry::Some { value, generation } => value.as_ref().map(|stored_asset| { let id = AssetId::Index { index: AssetIndex { generation: *generation, @@ -543,13 +636,13 @@ impl Assets { }, marker: PhantomData, }; - (id, v) + (id, A::AssetStorage::get_ref(stored_asset)) }), }) .chain( self.hash_map .iter() - .map(|(i, v)| (AssetId::Uuid { uuid: *i }, v)), + .map(|(i, v)| (AssetId::Uuid { uuid: *i }, A::AssetStorage::get_ref(v))), ) } @@ -622,11 +715,14 @@ impl Assets { pub struct AssetsMutIterator<'a, A: Asset> { queued_events: &'a mut Vec>, dense_storage: Enumerate>>, - hash_map: bevy_platform::collections::hash_map::IterMut<'a, Uuid, A>, + hash_map: bevy_platform::collections::hash_map::IterMut<'a, Uuid, StoredAsset>, } -impl<'a, A: Asset> Iterator for AssetsMutIterator<'a, A> { - type Item = (AssetId, &'a mut A); +impl<'a, A: Asset> Iterator for AssetsMutIterator<'a, A> +where + A::AssetStorage: AssetWriteStrategy, +{ + type Item = (AssetId, AssetMut<'a, A>); fn next(&mut self) -> Option { for (i, entry) in &mut self.dense_storage { @@ -644,7 +740,7 @@ impl<'a, A: Asset> Iterator for AssetsMutIterator<'a, A> { }; self.queued_events.push(AssetEvent::Modified { id }); if let Some(value) = value { - return Some((id, value)); + return Some((id, A::AssetStorage::get_mut(value))); } } } @@ -652,7 +748,7 @@ impl<'a, A: Asset> Iterator for AssetsMutIterator<'a, A> { if let Some((key, value)) = self.hash_map.next() { let id = AssetId::Uuid { uuid: *key }; self.queued_events.push(AssetEvent::Modified { id }); - Some((id, value)) + Some((id, A::AssetStorage::get_mut(value))) } else { None } diff --git a/crates/bevy_asset/src/handle.rs b/crates/bevy_asset/src/handle.rs index 1e03767c658dd..2032957ff7234 100644 --- a/crates/bevy_asset/src/handle.rs +++ b/crates/bevy_asset/src/handle.rs @@ -646,7 +646,7 @@ mod tests { /// `PartialReflect::reflect_clone`/`PartialReflect::to_dynamic` should increase the strong count of a strong handle #[test] fn strong_handle_reflect_clone() { - use crate::{AssetApp, AssetPlugin, Assets, VisitAssetDependencies}; + use crate::{AssetApp, AssetPlugin, Assets, StackAssetStorage, VisitAssetDependencies}; use bevy_app::App; use bevy_reflect::FromReflect; @@ -654,7 +654,9 @@ mod tests { struct MyAsset { value: u32, } - impl Asset for MyAsset {} + impl Asset for MyAsset { + type AssetStorage = StackAssetStorage; + } impl VisitAssetDependencies for MyAsset { fn visit_dependencies(&self, _visit: &mut impl FnMut(UntypedAssetId)) {} } diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index df47416193387..44b647ec271cc 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -167,8 +167,8 @@ pub mod prelude { #[doc(hidden)] pub use crate::{ - Asset, AssetApp, AssetEvent, AssetId, AssetMode, AssetPlugin, AssetServer, Assets, - DirectAssetAccessExt, Handle, UntypedHandle, + Asset, AssetApp, AssetEvent, AssetId, AssetMode, AssetMut, AssetPlugin, AssetRef, + AssetServer, AssetSnapshot, Assets, DirectAssetAccessExt, Handle, UntypedHandle, }; } @@ -185,6 +185,7 @@ mod path; mod reflect; mod render_asset; mod server; +mod storage; pub use assets::*; pub use bevy_asset_macros::Asset; @@ -203,6 +204,7 @@ pub use path::*; pub use reflect::*; pub use render_asset::*; pub use server::*; +pub use storage::*; pub use uuid; @@ -453,7 +455,9 @@ impl Plugin for AssetPlugin { label = "invalid `Asset`", note = "consider annotating `{Self}` with `#[derive(Asset)]`" )] -pub trait Asset: VisitAssetDependencies + TypePath + Send + Sync + 'static {} +pub trait Asset: VisitAssetDependencies + TypePath + Send + Sync + Sized + 'static { + type AssetStorage: AssetStorageStrategy; +} /// A trait for components that can be used as asset identifiers, e.g. handle wrappers. pub trait AsAssetId: Component { @@ -581,7 +585,8 @@ pub trait AssetApp { /// This enables reflection code to access assets. For detailed information, see the docs on [`ReflectAsset`] and [`ReflectHandle`]. fn register_asset_reflect(&mut self) -> &mut Self where - A: Asset + Reflect + FromReflect + GetTypeRegistration; + A: Asset + Reflect + FromReflect + GetTypeRegistration, + A::AssetStorage: AssetWriteStrategy; /// Preregisters a loader for the given extensions, that will block asset loads until a real loader /// is registered. fn preregister_asset_loader(&mut self, extensions: &[&str]) -> &mut Self; @@ -671,6 +676,7 @@ impl AssetApp for App { fn register_asset_reflect(&mut self) -> &mut Self where A: Asset + Reflect + FromReflect + GetTypeRegistration, + A::AssetStorage: AssetWriteStrategy, { let type_registry = self.world().resource::(); { @@ -1193,7 +1199,7 @@ mod tests { { let mut texts = app.world_mut().resource_mut::>(); - let a = texts.get_mut(a_id).unwrap(); + let mut a = texts.get_mut(a_id).unwrap(); a.text = "Changed".to_string(); } diff --git a/crates/bevy_asset/src/meta.rs b/crates/bevy_asset/src/meta.rs index 0e972261198cc..0e65251dfdf65 100644 --- a/crates/bevy_asset/src/meta.rs +++ b/crates/bevy_asset/src/meta.rs @@ -6,7 +6,7 @@ use alloc::{ use crate::{ loader::AssetLoader, processor::Process, Asset, AssetPath, DeserializeMetaError, - VisitAssetDependencies, + StackAssetStorage, VisitAssetDependencies, }; use downcast_rs::{impl_downcast, Downcast}; use ron::ser::PrettyConfig; @@ -189,7 +189,9 @@ impl Process for () { } } -impl Asset for () {} +impl Asset for () { + type AssetStorage = StackAssetStorage; +} impl VisitAssetDependencies for () { fn visit_dependencies(&self, _visit: &mut impl FnMut(bevy_asset::UntypedAssetId)) { diff --git a/crates/bevy_asset/src/reflect.rs b/crates/bevy_asset/src/reflect.rs index 063d3dccc8fb7..8b50c0f29e953 100644 --- a/crates/bevy_asset/src/reflect.rs +++ b/crates/bevy_asset/src/reflect.rs @@ -1,13 +1,96 @@ use alloc::boxed::Box; -use core::any::{Any, TypeId}; +use core::{ + any::{Any, TypeId}, + ops::{Deref, DerefMut}, +}; use bevy_ecs::world::{unsafe_world_cell::UnsafeWorldCell, World}; -use bevy_reflect::{FromReflect, FromType, PartialReflect, Reflect}; +use bevy_reflect::{FromReflect, FromType, PartialReflect, Reflect, ReflectMut, ReflectRef}; use crate::{ - Asset, AssetId, Assets, Handle, InvalidGenerationError, UntypedAssetId, UntypedHandle, + Asset, AssetId, AssetMut, AssetRef, AssetWriteStrategy, Assets, Handle, InvalidGenerationError, + UntypedAssetId, UntypedHandle, }; +/// The equivalent of `&dyn Reflect`. +/// +/// Holds a type that can pass out a reference to a reflectable type. +/// +/// Read-only reflection for the asset reference. +pub struct AssetRefReflect<'a> { + asset_ref: Box + 'a>, +} + +impl<'a> AssetRefReflect<'a> { + pub fn new(asset_ref: AssetRef<'a, A>) -> Self { + /// Wrapper that adapts an `AssetRef` to deref to `dyn Reflect` + struct AssetRefWrapper<'a, A: Asset + Reflect> { + asset_ref: AssetRef<'a, A>, + } + + impl<'a, A: Asset + Reflect> Deref for AssetRefWrapper<'a, A> { + type Target = dyn Reflect; + + fn deref(&self) -> &Self::Target { + self.asset_ref.as_reflect() + } + } + + Self { + asset_ref: Box::new(AssetRefWrapper::<'a, A> { asset_ref }), + } + } + #[inline] + pub fn reflect(&mut self) -> ReflectRef<'_> { + self.asset_ref.as_ref().reflect_ref() + } +} + +/// The equivalent of `&mut dyn Reflect`. +/// +/// Holds a type that can pass out a reference to a reflectable type. +/// +/// Read-write reflection for the asset reference. +pub struct AssetMutReflect<'a> { + asset_mut: Box + 'a>, +} + +impl<'a> AssetMutReflect<'a> { + pub fn new(asset_mut: AssetMut<'a, A>) -> Self + where + A::AssetStorage: AssetWriteStrategy, + { + /// Wrapper that adapts an `AssetMut` to deref to `dyn mut Reflect` + struct AssetMutWrapper<'a, A: Asset> + Reflect> { + asset_mut: AssetMut<'a, A>, + } + + impl<'a, A: Asset> + Reflect> Deref for AssetMutWrapper<'a, A> { + type Target = dyn Reflect; + + fn deref(&self) -> &Self::Target { + self.asset_mut.as_reflect() + } + } + + impl<'a, A: Asset> + Reflect> DerefMut + for AssetMutWrapper<'a, A> + { + fn deref_mut(&mut self) -> &mut Self::Target { + self.asset_mut.as_reflect_mut() + } + } + + Self { + asset_mut: Box::new(AssetMutWrapper::<'a, A> { asset_mut }), + } + } + #[inline] + pub fn reflect_mut(&mut self) -> ReflectMut<'_> { + self.asset_mut.as_mut().reflect_mut() + } +} + /// Type data for the [`TypeRegistry`](bevy_reflect::TypeRegistry) used to operate on reflected [`Asset`]s. /// /// This type provides similar methods to [`Assets`] like [`get`](ReflectAsset::get), @@ -20,17 +103,18 @@ pub struct ReflectAsset { handle_type_id: TypeId, assets_resource_type_id: TypeId, - get: fn(&World, UntypedAssetId) -> Option<&dyn Reflect>, + get: fn(&World, UntypedAssetId) -> Option, // SAFETY: // - may only be called with an [`UnsafeWorldCell`] which can be used to access the corresponding `Assets` resource mutably // - may only be used to access **at most one** access at once - get_unchecked_mut: unsafe fn(UnsafeWorldCell<'_>, UntypedAssetId) -> Option<&mut dyn Reflect>, + get_unchecked_mut: + for<'w> unsafe fn(UnsafeWorldCell<'w>, UntypedAssetId) -> Option>, add: fn(&mut World, &dyn PartialReflect) -> UntypedHandle, insert: fn(&mut World, UntypedAssetId, &dyn PartialReflect) -> Result<(), InvalidGenerationError>, len: fn(&World) -> usize, ids: for<'w> fn(&'w World) -> Box + 'w>, - remove: fn(&mut World, UntypedAssetId) -> Option>, + remove: fn(&mut World, UntypedAssetId), // -> Option>, } impl ReflectAsset { @@ -49,7 +133,7 @@ impl ReflectAsset { &self, world: &'w World, asset_id: impl Into, - ) -> Option<&'w dyn Reflect> { + ) -> Option> { (self.get)(world, asset_id.into()) } @@ -58,7 +142,7 @@ impl ReflectAsset { &self, world: &'w mut World, asset_id: impl Into, - ) -> Option<&'w mut dyn Reflect> { + ) -> Option> { // SAFETY: unique world access #[expect( unsafe_code, @@ -104,7 +188,7 @@ impl ReflectAsset { &self, world: UnsafeWorldCell<'w>, asset_id: impl Into, - ) -> Option<&'w mut dyn Reflect> { + ) -> Option> { // SAFETY: requirements are deferred to the caller unsafe { (self.get_unchecked_mut)(world, asset_id.into()) } } @@ -124,12 +208,9 @@ impl ReflectAsset { } /// Equivalent of [`Assets::remove`] - pub fn remove( - &self, - world: &mut World, - asset_id: impl Into, - ) -> Option> { - (self.remove)(world, asset_id.into()) + pub fn remove(&self, world: &mut World, asset_id: impl Into) { + // -> Option> { + (self.remove)(world, asset_id.into()); } /// Equivalent of [`Assets::len`] @@ -148,23 +229,27 @@ impl ReflectAsset { } } -impl FromType for ReflectAsset { +impl FromType for ReflectAsset +where + A::AssetStorage: AssetWriteStrategy, +{ fn from_type() -> Self { ReflectAsset { handle_type_id: TypeId::of::>(), assets_resource_type_id: TypeId::of::>(), get: |world, asset_id| { - let assets = world.resource::>(); - let asset = assets.get(asset_id.typed_debug_checked()); - asset.map(|asset| asset as &dyn Reflect) + world + .resource::>() + .get(asset_id.typed_debug_checked()) + .map(|asset_ref| AssetRefReflect::new::(asset_ref)) }, get_unchecked_mut: |world, asset_id| { // SAFETY: `get_unchecked_mut` must be called with `UnsafeWorldCell` having access to `Assets`, // and must ensure to only have at most one reference to it live at all times. #[expect(unsafe_code, reason = "Uses `UnsafeWorldCell::get_resource_mut()`.")] let assets = unsafe { world.get_resource_mut::>().unwrap().into_inner() }; - let asset = assets.get_mut(asset_id.typed_debug_checked()); - asset.map(|asset| asset as &mut dyn Reflect) + let asset_mut = assets.get_mut(asset_id.typed_debug_checked()); + asset_mut.map(|asset_mut| AssetMutReflect::new::(asset_mut)) }, add: |world, value| { let mut assets = world.resource_mut::>(); @@ -188,8 +273,9 @@ impl FromType for ReflectAsset { }, remove: |world, asset_id| { let mut assets = world.resource_mut::>(); - let value = assets.remove(asset_id.typed_debug_checked()); - value.map(|value| Box::new(value) as Box) + assets.remove(asset_id.typed_debug_checked()); + // let value = assets.remove(asset_id.typed_debug_checked()); + // value.map(|value| Box::new(value) as Box) }, } } diff --git a/crates/bevy_asset/src/storage/arced.rs b/crates/bevy_asset/src/storage/arced.rs new file mode 100644 index 0000000000000..d11174a01aa1b --- /dev/null +++ b/crates/bevy_asset/src/storage/arced.rs @@ -0,0 +1,77 @@ +use alloc::sync::Arc; + +use super::{AssetSnapshotStrategy, AssetStorageStrategy, AssetWriteStrategy}; + +macro_rules! panic_asset_erased { + () => { + panic!( + "This {} asset has been erased", + ::core::any::type_name::() + ) + }; +} + +/// This storage strategy wraps assets in an [`Arc`] so that they can be shared between threads. +/// This provides great read performance, at the expense of needing to clone the asset when the +/// asset is mutated (if there are outstanding references). +/// +/// ## Ideal Usage +/// Best for heavy asset types (like images and meshes) that don't experience repeated updates. +pub struct ArcedAssetStorage; + +impl AssetStorageStrategy for ArcedAssetStorage { + type AssetStorage = Option>; + + type AssetRef<'a> + = &'a A + where + A: 'a; + + #[inline] + fn new(asset: A) -> Self::AssetStorage { + Some(Arc::new(asset)) + } + #[inline] + fn get_ref<'a>(stored_asset: &'a Self::AssetStorage) -> &'a A { + stored_asset + .as_ref() + .unwrap_or_else(|| panic_asset_erased!()) + .as_ref() + } + #[inline] + fn into_inner(stored_asset: Self::AssetStorage) -> Option { + Arc::into_inner(stored_asset?) + } +} + +impl AssetWriteStrategy for ArcedAssetStorage { + type AssetMut<'a> + = &'a mut A + where + A: 'a; + + #[inline] + fn get_mut<'a>(stored_asset: &'a mut Self::AssetStorage) -> &'a mut A { + Arc::make_mut( + stored_asset + .as_mut() + .unwrap_or_else(|| panic_asset_erased!()), + ) + } +} + +impl AssetSnapshotStrategy for ArcedAssetStorage { + type AssetSnapshot = Arc; + + #[inline] + fn get_snapshot(stored_asset: &mut Self::AssetStorage) -> Arc { + stored_asset + .as_ref() + .unwrap_or_else(|| panic_asset_erased!()) + .clone() + } + #[inline] + fn get_snapshot_erased(stored_asset: &mut Self::AssetStorage) -> Self::AssetSnapshot { + stored_asset.take().unwrap_or_else(|| panic_asset_erased!()) + } +} diff --git a/crates/bevy_asset/src/storage/boxed.rs b/crates/bevy_asset/src/storage/boxed.rs new file mode 100644 index 0000000000000..95bebcc5b209e --- /dev/null +++ b/crates/bevy_asset/src/storage/boxed.rs @@ -0,0 +1,72 @@ +use alloc::boxed::Box; + +use super::{AssetSnapshotStrategy, AssetStorageStrategy, AssetWriteStrategy}; + +macro_rules! panic_asset_erased { + () => { + panic!( + "This {} asset has been erased", + ::core::any::type_name::() + ) + }; +} + +/// This storage strategy wraps assets in a [`Box`]. This is less preferable than [`StackAssetStorage`], +/// except for cases when the stack size of the asset type is large. Boxing reduces the performance cost +/// of resizing the inner storage of `Assets` when assets are added and the capacity is exceeded. +pub struct BoxedAssetStorage; + +impl AssetStorageStrategy for BoxedAssetStorage { + type AssetStorage = Option>; + + type AssetRef<'a> + = &'a A + where + A: 'a; + + #[inline] + fn new(asset: A) -> Self::AssetStorage { + Some(Box::new(asset)) + } + #[inline] + fn get_ref<'a>(stored_asset: &'a Self::AssetStorage) -> &'a A { + stored_asset + .as_ref() + .unwrap_or_else(|| panic_asset_erased!()) + } + #[inline] + fn into_inner(stored_asset: Self::AssetStorage) -> Option { + Some(*stored_asset?) + } +} + +impl AssetWriteStrategy for BoxedAssetStorage { + type AssetMut<'a> + = &'a mut A + where + A: 'a; + + #[inline] + fn get_mut<'a>(stored_asset: &'a mut Self::AssetStorage) -> &'a mut A { + stored_asset + .as_mut() + .unwrap_or_else(|| panic_asset_erased!()) + } +} + +impl AssetSnapshotStrategy for BoxedAssetStorage { + type AssetSnapshot = Box; + + #[inline] + fn get_snapshot(stored_asset: &mut Self::AssetStorage) -> Box { + Box::clone( + stored_asset + .as_ref() + .unwrap_or_else(|| panic_asset_erased!()), + ) + } + #[inline] + fn get_snapshot_erased(stored_asset: &mut Self::AssetStorage) -> Self::AssetSnapshot { + stored_asset.take().unwrap_or_else(|| panic_asset_erased!()) + } +} diff --git a/crates/bevy_asset/src/storage/hybrid.rs b/crates/bevy_asset/src/storage/hybrid.rs new file mode 100644 index 0000000000000..388f07f4fe871 --- /dev/null +++ b/crates/bevy_asset/src/storage/hybrid.rs @@ -0,0 +1,318 @@ +use alloc::sync::Arc; +use bevy_platform::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; +use core::{ + borrow::{Borrow, BorrowMut}, + fmt::{Debug, Display}, + ops::{Deref, DerefMut}, +}; + +use super::{AssetAsyncStrategy, AssetSnapshotStrategy, AssetStorageStrategy, AssetWriteStrategy}; + +macro_rules! panic_asset_erased { + () => { + panic!( + "This {} asset has been erased", + ::core::any::type_name::() + ) + }; +} + +/// This storage strategy provides async read/write access to assets. +/// +/// This is achieved by storing the asset on the stack by default, and upgrading to an `Arc` or `RwLock` when needed. +/// This approach allows assets that *don't* need to be shared across threads to not have to pay the potential performance cost +/// of locking. Also, for asset types that never request a `RwLock`, the compiler's optimizer should be able to optimize-away +/// the lock completely. +/// +/// ## Ideal Usage +/// Best for heavy asset types (like images and meshes) that might need to be read or written in async contexts. +pub struct HybridAssetStorage; + +pub enum HybridStorage { + /// Default storage state (low overhead) + Owned(A), + /// Referenced counting enabled (this asset is being shared across threads) + UpgradedToArc(Arc), + /// This asset has been upgraded to a mutex so that it can be written in async contexts + UpgradedToArcRwLock(Arc>), + /// The asset's been removed + Erased, +} + +pub enum HybridStorageRef<'a, A> { + Direct(&'a A), + Guard(RwLockReadGuard<'a, A>), +} + +impl<'a, A> Deref for HybridStorageRef<'a, A> { + type Target = A; + #[inline] + fn deref(&self) -> &Self::Target { + match self { + Self::Direct(asset) => asset, + Self::Guard(asset) => asset.deref(), + } + } +} + +impl<'a, A> Borrow for HybridStorageRef<'a, A> { + #[inline] + fn borrow(&self) -> &A { + match self { + Self::Direct(asset) => asset, + Self::Guard(asset) => asset.deref(), + } + } +} + +pub enum HybridStorageMut<'a, A> { + Direct(&'a mut A), + Guard(RwLockWriteGuard<'a, A>), +} + +impl<'a, A> Deref for HybridStorageMut<'a, A> { + type Target = A; + #[inline] + fn deref(&self) -> &Self::Target { + match self { + Self::Direct(asset) => asset, + Self::Guard(asset) => asset.deref(), + } + } +} + +impl<'a, A> DerefMut for HybridStorageMut<'a, A> { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + match self { + Self::Direct(asset) => asset, + Self::Guard(asset) => asset.deref_mut(), + } + } +} + +impl<'a, A> Borrow for HybridStorageMut<'a, A> { + #[inline] + fn borrow(&self) -> &A { + match self { + Self::Direct(asset) => asset, + Self::Guard(asset) => asset.deref(), + } + } +} + +impl<'a, A> BorrowMut for HybridStorageMut<'a, A> { + #[inline] + fn borrow_mut(&mut self) -> &mut A { + match self { + Self::Direct(asset) => asset, + Self::Guard(asset) => asset.deref_mut(), + } + } +} + +impl AssetStorageStrategy for HybridAssetStorage { + type AssetStorage = HybridStorage; + + type AssetRef<'a> + = HybridStorageRef<'a, A> + where + A: 'a; + + #[inline] + fn new(asset: A) -> Self::AssetStorage { + HybridStorage::Owned(asset) + } + #[inline] + fn get_ref(stored_asset: &Self::AssetStorage) -> Self::AssetRef<'_> { + match stored_asset { + HybridStorage::Owned(asset) => HybridStorageRef::Direct(asset), + HybridStorage::UpgradedToArc(asset) => HybridStorageRef::Direct(asset), + HybridStorage::UpgradedToArcRwLock(asset) => { + HybridStorageRef::Guard(asset.read().unwrap()) + } + HybridStorage::Erased => panic_asset_erased!(), + } + } + #[inline] + fn into_inner(stored_asset: Self::AssetStorage) -> Option { + match stored_asset { + HybridStorage::Owned(asset) => Some(asset), + HybridStorage::UpgradedToArc(asset) => Arc::into_inner(asset), + HybridStorage::UpgradedToArcRwLock(asset) => Arc::into_inner(asset)?.into_inner().ok(), + HybridStorage::Erased => None, + } + } +} + +impl AssetWriteStrategy for HybridAssetStorage { + type AssetMut<'a> + = HybridStorageMut<'a, A> + where + A: 'a; + + #[inline] + fn get_mut<'a>(stored_asset: &'a mut Self::AssetStorage) -> Self::AssetMut<'a> { + match stored_asset { + HybridStorage::Owned(asset) => HybridStorageMut::Direct(asset), + HybridStorage::UpgradedToArc(asset) => HybridStorageMut::Direct(Arc::make_mut(asset)), + HybridStorage::UpgradedToArcRwLock(asset) => { + HybridStorageMut::Guard(asset.write().unwrap()) + } + HybridStorage::Erased => panic_asset_erased!(), + } + } +} + +impl AssetAsyncStrategy for HybridAssetStorage { + fn get_arc(stored_asset: &mut Self::AssetStorage) -> Arc { + match stored_asset { + HybridStorage::Owned(..) => { + // Transition to UpgradedToArc (take the asset without cloning) + let owned = core::mem::replace(stored_asset, HybridStorage::Erased); + if let HybridStorage::Owned(asset) = owned { + let new_arc = Arc::new(asset); + *stored_asset = HybridStorage::UpgradedToArc(Arc::clone(&new_arc)); + new_arc + } else { + unreachable!() + } + } + HybridStorage::UpgradedToArc(asset) => Arc::clone(asset), + HybridStorage::UpgradedToArcRwLock(asset) => { + // If there's a lock that exists, we only can just return a snapshot + Arc::new(asset.read().unwrap().clone()) + } + HybridStorage::Erased => panic_asset_erased!(), + } + } + + fn get_arc_rwlock(stored_asset: &mut Self::AssetStorage) -> Arc> { + match stored_asset { + HybridStorage::Owned(..) => { + // Transition to UpgradedToArcRwLock + let owned = core::mem::replace(stored_asset, HybridStorage::Erased); + if let HybridStorage::Owned(asset) = owned { + let new_arc_rwlock = Arc::new(RwLock::new(asset)); + *stored_asset = HybridStorage::UpgradedToArcRwLock(Arc::clone(&new_arc_rwlock)); + new_arc_rwlock + } else { + unreachable!() + } + } + HybridStorage::UpgradedToArc(..) => { + // Transition to UpgradedToArcRwLock + let arc = core::mem::replace(stored_asset, HybridStorage::Erased); + if let HybridStorage::UpgradedToArc(arc) = arc { + // Try to unwrap the Arc to get ownership, otherwise clone the inner value + let asset = Arc::try_unwrap(arc).unwrap_or_else(|arc| (*arc).clone()); + let new_arc_rwlock = Arc::new(RwLock::new(asset)); + *stored_asset = HybridStorage::UpgradedToArcRwLock(Arc::clone(&new_arc_rwlock)); + new_arc_rwlock + } else { + unreachable!() + } + } + HybridStorage::UpgradedToArcRwLock(asset) => Arc::clone(asset), + HybridStorage::Erased => panic_asset_erased!(), + } + } +} + +impl AssetSnapshotStrategy for HybridAssetStorage { + type AssetSnapshot = HybridSnapshot; + + #[inline] + fn get_snapshot(stored_asset: &mut Self::AssetStorage) -> HybridSnapshot { + HybridSnapshot::Arc(Self::get_arc(stored_asset)) + } + fn get_snapshot_erased(stored_asset: &mut Self::AssetStorage) -> HybridSnapshot { + match stored_asset { + HybridStorage::Owned(..) => { + // Take ownership and transition to Erased + let stored_asset = core::mem::replace(stored_asset, HybridStorage::Erased); + if let HybridStorage::Owned(asset) = stored_asset { + HybridSnapshot::Owned(asset) + } else { + unreachable!() + } + } + HybridStorage::UpgradedToArc(..) => { + // Try to take ownership and transition to Erased + let stored_asset = core::mem::replace(stored_asset, HybridStorage::Erased); + if let HybridStorage::UpgradedToArc(arc) = stored_asset { + match Arc::try_unwrap(arc) { + Ok(owned_asset) => HybridSnapshot::Owned(owned_asset), + Err(arc) => HybridSnapshot::Arc(arc), + } + } else { + unreachable!() + } + } + HybridStorage::UpgradedToArcRwLock(..) => { + let stored_asset = core::mem::replace(stored_asset, HybridStorage::Erased); + if let HybridStorage::UpgradedToArcRwLock(arc_rw_lock) = stored_asset { + match Arc::try_unwrap(arc_rw_lock) { + Ok(rw_lock) => HybridSnapshot::Owned(rw_lock.into_inner().unwrap()), + Err(arc_rw_lock) => { + HybridSnapshot::Owned(arc_rw_lock.read().unwrap().clone()) + } + } + } else { + unreachable!() + } + } + HybridStorage::Erased => panic_asset_erased!(), + } + } +} + +pub enum HybridSnapshot { + Arc(Arc), + Owned(A), +} + +impl HybridSnapshot { + #[inline] + pub fn into_inner(self) -> A { + match self { + Self::Owned(asset) => asset, + Self::Arc(asset) => Arc::try_unwrap(asset).unwrap_or_else(|arc| (*arc).clone()), + } + } +} + +impl Deref for HybridSnapshot { + type Target = A; + #[inline] + fn deref(&self) -> &Self::Target { + match self { + Self::Owned(asset) => asset, + Self::Arc(asset) => asset.as_ref(), + } + } +} + +impl Debug for HybridSnapshot +where + A: Debug, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Owned(asset) => asset.fmt(f), + Self::Arc(asset) => asset.fmt(f), + } + } +} + +impl Display for HybridSnapshot +where + A: Display, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Owned(asset) => asset.fmt(f), + Self::Arc(asset) => asset.fmt(f), + } + } +} diff --git a/crates/bevy_asset/src/storage/mod.rs b/crates/bevy_asset/src/storage/mod.rs new file mode 100644 index 0000000000000..49572dd6779bf --- /dev/null +++ b/crates/bevy_asset/src/storage/mod.rs @@ -0,0 +1,78 @@ +use crate::Asset; +use alloc::sync::Arc; +use bevy_platform::sync::RwLock; +use core::{ + borrow::{Borrow, BorrowMut}, + ops::{Deref, DerefMut}, +}; + +mod arced; +mod boxed; +mod hybrid; +mod stack; + +pub use arced::*; +pub use boxed::*; +pub use hybrid::*; +pub use stack::*; + +#[expect(type_alias_bounds, reason = "Type alias generics not yet stable")] +pub type StoredAsset = >::AssetStorage; + +/// A reference to a stored asset. This can be dereferenced to `&A`. +#[expect(type_alias_bounds, reason = "Type alias generics not yet stable")] +pub type AssetRef<'a, A: Asset> = >::AssetRef<'a>; + +/// A mutable reference to a stored asset. This can be dereferenced to `&mut A`. +#[expect(type_alias_bounds, reason = "Type alias generics not yet stable")] +pub type AssetMut<'a, A: Asset> = >::AssetMut<'a>; + +/// A snapshot of an asset. This will be a clone of the asset `A`, or `Arc`, depending on the storage strategy. +#[expect(type_alias_bounds, reason = "Type alias generics not yet stable")] +pub type AssetSnapshot = >::AssetSnapshot; + +/// Defines how an asset `A` is stored internally. +pub trait AssetStorageStrategy { + type AssetStorage: Send + Sync; + type AssetRef<'a>: Borrow + Deref + where + Self: 'a, + A: 'a; + fn new(asset: A) -> Self::AssetStorage; + + /// Attempts to take ownership of the asset. + /// + /// This will return `None` if the asset has been erased, or if the asset has outstanding references. + fn into_inner(stored_asset: Self::AssetStorage) -> Option; + + /// Returns a reference to the asset. + fn get_ref<'a>(stored_asset: &'a Self::AssetStorage) -> Self::AssetRef<'a>; +} + +pub trait AssetWriteStrategy: AssetStorageStrategy { + type AssetMut<'a>: BorrowMut + Deref + DerefMut + where + Self: 'a, + A: 'a; + + /// Returns a mutable reference to the asset. + fn get_mut<'a>(stored_asset: &'a mut Self::AssetStorage) -> Self::AssetMut<'a>; +} + +pub trait AssetSnapshotStrategy: AssetStorageStrategy { + type AssetSnapshot: Send + Sync + Deref; + + /// Returns a snapshot of the asset, which is a clone of the asset `A` (or an `Arc` clone, depending on the storage strategy). + fn get_snapshot(stored_asset: &mut Self::AssetStorage) -> Self::AssetSnapshot; + + /// Instead of returning a clone of the asset or an Arc clone like [`StoredAssetEntry::snapshot`], + /// this will take ownership of the asset and put the entry in [`Assets`] into an erased state. + /// + /// Future attempts to get the asset will fail. + fn get_snapshot_erased(stored_asset: &mut Self::AssetStorage) -> Self::AssetSnapshot; +} + +pub trait AssetAsyncStrategy: AssetStorageStrategy { + fn get_arc(stored_asset: &mut Self::AssetStorage) -> Arc; + fn get_arc_rwlock(stored_asset: &mut Self::AssetStorage) -> Arc>; +} diff --git a/crates/bevy_asset/src/storage/stack.rs b/crates/bevy_asset/src/storage/stack.rs new file mode 100644 index 0000000000000..a644174f87ad6 --- /dev/null +++ b/crates/bevy_asset/src/storage/stack.rs @@ -0,0 +1,123 @@ +use core::{ + borrow::{Borrow, BorrowMut}, + fmt::Debug, + ops::{Deref, DerefMut}, +}; + +use super::{AssetSnapshotStrategy, AssetStorageStrategy, AssetWriteStrategy}; + +macro_rules! panic_asset_erased { + () => { + panic!( + "This {} asset has been erased", + ::core::any::type_name::() + ) + }; +} + +pub struct StackAsset(A); + +impl StackAsset { + #[inline] + pub fn into_inner(self) -> A { + self.0 + } +} + +impl Deref for StackAsset { + type Target = A; + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for StackAsset { + #[inline] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Borrow for StackAsset { + #[inline] + fn borrow(&self) -> &A { + &self.0 + } +} + +impl BorrowMut for StackAsset { + #[inline] + fn borrow_mut(&mut self) -> &mut A { + &mut self.0 + } +} + +impl Debug for StackAsset +where + A: Debug, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.0.fmt(f) + } +} + +/// Best for light asset types that are cheap to clone (e.g. materials, config, etc) +pub struct StackAssetStorage; + +impl AssetStorageStrategy for StackAssetStorage { + type AssetStorage = Option>; + + type AssetRef<'a> + = &'a A + where + A: 'a; + + #[inline] + fn new(asset: A) -> Self::AssetStorage { + Some(StackAsset(asset)) + } + #[inline] + fn get_ref<'a>(stored_asset: &'a Self::AssetStorage) -> &'a A { + stored_asset + .as_ref() + .unwrap_or_else(|| panic_asset_erased!()) + } + #[inline] + fn into_inner(stored_asset: Self::AssetStorage) -> Option { + stored_asset.map(|stack_asset| stack_asset.0) + } +} + +impl AssetWriteStrategy for StackAssetStorage { + type AssetMut<'a> + = &'a mut A + where + A: 'a; + + #[inline] + fn get_mut<'a>(stored_asset: &'a mut Self::AssetStorage) -> &'a mut A { + stored_asset + .as_mut() + .unwrap_or_else(|| panic_asset_erased!()) + } +} + +impl AssetSnapshotStrategy for StackAssetStorage { + type AssetSnapshot = StackAsset; + + #[inline] + fn get_snapshot(stored_asset: &mut Self::AssetStorage) -> StackAsset { + StackAsset( + stored_asset + .as_ref() + .unwrap_or_else(|| panic_asset_erased!()) + .0 + .clone(), + ) + } + #[inline] + fn get_snapshot_erased(stored_asset: &mut Self::AssetStorage) -> StackAsset { + stored_asset.take().unwrap_or_else(|| panic_asset_erased!()) + } +} diff --git a/crates/bevy_gizmos_render/src/lib.rs b/crates/bevy_gizmos_render/src/lib.rs index 42e73e9192c00..d8796dbca7237 100755 --- a/crates/bevy_gizmos_render/src/lib.rs +++ b/crates/bevy_gizmos_render/src/lib.rs @@ -25,6 +25,7 @@ mod pipeline_2d; mod pipeline_3d; use bevy_app::{App, Plugin}; +use bevy_asset::AssetSnapshot; use bevy_ecs::{ resource::Resource, schedule::{IntoScheduleConfigs, SystemSet}, @@ -230,11 +231,11 @@ impl RenderAsset for GpuLineGizmo { type Param = SRes; fn prepare_asset( - gizmo: Self::SourceAsset, + gizmo: AssetSnapshot, _: AssetId, render_device: &mut SystemParamItem, _: Option<&Self>, - ) -> Result> { + ) -> Result>> { let list_position_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { usage: BufferUsages::VERTEX, label: Some("LineGizmo Position Buffer"), diff --git a/crates/bevy_image/src/texture_atlas_builder.rs b/crates/bevy_image/src/texture_atlas_builder.rs index 98da24ac41611..f302a0d6212b5 100644 --- a/crates/bevy_image/src/texture_atlas_builder.rs +++ b/crates/bevy_image/src/texture_atlas_builder.rs @@ -1,4 +1,4 @@ -use bevy_asset::{AssetId, RenderAssetUsages}; +use bevy_asset::{AssetId, AssetRef, RenderAssetUsages}; use bevy_math::{URect, UVec2}; use bevy_platform::collections::HashMap; use rectangle_pack::{ @@ -29,13 +29,12 @@ pub enum TextureAtlasBuilderError { TextureAccess(#[from] TextureAccessError), } -#[derive(Debug)] #[must_use] /// A builder which is used to create a texture atlas from many individual /// sprites. pub struct TextureAtlasBuilder<'a> { /// Collection of texture's asset id (optional) and image data to be packed into an atlas - textures_to_place: Vec<(Option>, &'a Image)>, + textures_to_place: Vec<(Option>, AssetRef<'a, Image>)>, /// The initial atlas size in pixels. initial_size: UVec2, /// The absolute maximum size of the texture atlas in pixels. @@ -95,7 +94,7 @@ impl<'a> TextureAtlasBuilder<'a> { pub fn add_texture( &mut self, image_id: Option>, - texture: &'a Image, + texture: AssetRef<'a, Image>, ) -> &mut Self { self.textures_to_place.push((image_id, texture)); self diff --git a/crates/bevy_pbr/src/light_probe/generate.rs b/crates/bevy_pbr/src/light_probe/generate.rs index a944513bd3128..a24a1b23cefbc 100644 --- a/crates/bevy_pbr/src/light_probe/generate.rs +++ b/crates/bevy_pbr/src/light_probe/generate.rs @@ -1105,24 +1105,27 @@ pub fn generate_environment_map_light( query: Query<(Entity, &GeneratedEnvironmentMapLight), Without>, ) { for (entity, filtered_env_map) in &query { - // Validate and fetch the source cubemap so we can size our targets correctly - let Some(src_image) = images.get(&filtered_env_map.environment_map) else { - // Texture not ready yet – try again next frame - continue; - }; + let base_size = { + // Validate and fetch the source cubemap so we can size our targets correctly + let Some(src_image) = images.get(&filtered_env_map.environment_map) else { + // Texture not ready yet – try again next frame + continue; + }; - let base_size = src_image.texture_descriptor.size.width; + let base_size = src_image.texture_descriptor.size.width; - // Sanity checks – square, power-of-two, ≤ 8192 - if src_image.texture_descriptor.size.height != base_size - || !base_size.is_power_of_two() - || base_size > 8192 - { - panic!( - "GeneratedEnvironmentMapLight source cubemap must be square power-of-two ≤ 8192, got {}×{}", - base_size, src_image.texture_descriptor.size.height - ); - } + // Sanity checks – square, power-of-two, ≤ 8192 + if src_image.texture_descriptor.size.height != base_size + || !base_size.is_power_of_two() + || base_size > 8192 + { + panic!( + "GeneratedEnvironmentMapLight source cubemap must be square power-of-two ≤ 8192, got {}×{}", + base_size, src_image.texture_descriptor.size.height + ); + } + base_size + }; let mip_count = compute_mip_count(base_size); diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index c49619c5179ee..4a349daaf2fa2 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -4,7 +4,10 @@ use crate::material_bind_groups::{ use crate::*; use alloc::sync::Arc; use bevy_asset::prelude::AssetChanged; -use bevy_asset::{Asset, AssetEventSystems, AssetId, AssetServer, UntypedAssetId}; +use bevy_asset::{ + Asset, AssetEventSystems, AssetId, AssetServer, AssetSnapshot, AssetSnapshotStrategy, + UntypedAssetId, +}; use bevy_camera::visibility::ViewVisibility; use bevy_camera::ScreenSpaceTransmissionQuality; use bevy_core_pipeline::deferred::{AlphaMask3dDeferred, Opaque3dDeferred}; @@ -135,7 +138,9 @@ pub const MATERIAL_BIND_GROUP_INDEX: usize = 3; /// @group(#{MATERIAL_BIND_GROUP}) @binding(1) var color_texture: texture_2d; /// @group(#{MATERIAL_BIND_GROUP}) @binding(2) var color_sampler: sampler; /// ``` -pub trait Material: Asset + AsBindGroup + Clone + Sized { +pub trait Material: + Asset> + AsBindGroup + Clone + Sized +{ /// Returns this material's vertex shader. If [`ShaderRef::Default`] is returned, the default mesh vertex shader /// will be used. fn vertex_shader() -> ShaderRef { @@ -1635,7 +1640,7 @@ where ); fn prepare_asset( - material: Self::SourceAsset, + material: AssetSnapshot, material_id: AssetId, ( render_device, @@ -1655,7 +1660,7 @@ where asset_server, material_param, ): &mut SystemParamItem, - ) -> Result> { + ) -> Result>> { let shadows_enabled = M::enable_shadows(); let prepass_enabled = M::enable_prepass(); diff --git a/crates/bevy_pbr/src/medium.rs b/crates/bevy_pbr/src/medium.rs index 0c08e00633ae5..e3863f08c30fa 100644 --- a/crates/bevy_pbr/src/medium.rs +++ b/crates/bevy_pbr/src/medium.rs @@ -2,7 +2,7 @@ use alloc::{borrow::Cow, sync::Arc}; use core::f32::{self, consts::PI}; use bevy_app::{App, Plugin}; -use bevy_asset::{Asset, AssetApp, AssetId}; +use bevy_asset::{Asset, AssetApp, AssetId, AssetSnapshot}; use bevy_ecs::{ resource::Resource, system::{Commands, Res, SystemParamItem}, @@ -415,11 +415,12 @@ impl RenderAsset for GpuScatteringMedium { type Param = (Res<'static, RenderDevice>, Res<'static, RenderQueue>); fn prepare_asset( - source_asset: Self::SourceAsset, + source_asset: AssetSnapshot, _asset_id: AssetId, (render_device, render_queue): &mut SystemParamItem, _previous_asset: Option<&Self>, - ) -> Result> { + ) -> Result>> { + let source_asset = source_asset.into_inner(); let mut density: Vec = Vec::with_capacity(2 * source_asset.falloff_resolution as usize); diff --git a/crates/bevy_pbr/src/render/skin.rs b/crates/bevy_pbr/src/render/skin.rs index 75c424311a056..381e64e34ec8e 100644 --- a/crates/bevy_pbr/src/render/skin.rs +++ b/crates/bevy_pbr/src/render/skin.rs @@ -543,9 +543,11 @@ fn add_skin( for (joint_index, &joint) in skinned_mesh.joints.iter().enumerate() { // Calculate the initial joint matrix. let skinned_mesh_inverse_bindpose = - skinned_mesh_inverse_bindposes.and_then(|skinned_mesh_inverse_bindposes| { - skinned_mesh_inverse_bindposes.get(joint_index) - }); + skinned_mesh_inverse_bindposes + .as_ref() + .and_then(|skinned_mesh_inverse_bindposes| { + skinned_mesh_inverse_bindposes.get(joint_index) + }); let joint_matrix = match (skinned_mesh_inverse_bindpose, joints.get(joint)) { (Some(skinned_mesh_inverse_bindpose), Ok(transform)) => { transform.affine() * *skinned_mesh_inverse_bindpose diff --git a/crates/bevy_pbr/src/wireframe.rs b/crates/bevy_pbr/src/wireframe.rs index 0468b7c0681ad..7c266a1963f2e 100644 --- a/crates/bevy_pbr/src/wireframe.rs +++ b/crates/bevy_pbr/src/wireframe.rs @@ -6,7 +6,7 @@ use crate::{ use bevy_app::{App, Plugin, PostUpdate, Startup, Update}; use bevy_asset::{ embedded_asset, load_embedded_asset, prelude::AssetChanged, AsAssetId, Asset, AssetApp, - AssetEventSystems, AssetId, AssetServer, Assets, Handle, UntypedAssetId, + AssetEventSystems, AssetId, AssetServer, AssetSnapshot, Assets, Handle, UntypedAssetId, }; use bevy_camera::{visibility::ViewVisibility, Camera, Camera3d}; use bevy_color::{Color, ColorToComponents}; @@ -483,11 +483,11 @@ impl RenderAsset for RenderWireframeMaterial { type Param = (); fn prepare_asset( - source_asset: Self::SourceAsset, + source_asset: AssetSnapshot, _asset_id: AssetId, _param: &mut SystemParamItem, _previous_asset: Option<&Self>, - ) -> Result> { + ) -> Result>> { Ok(RenderWireframeMaterial { color: source_asset.color.to_linear().to_f32_array(), }) diff --git a/crates/bevy_post_process/src/auto_exposure/compensation_curve.rs b/crates/bevy_post_process/src/auto_exposure/compensation_curve.rs index c7c4fcbecb174..f8a0bccf79012 100644 --- a/crates/bevy_post_process/src/auto_exposure/compensation_curve.rs +++ b/crates/bevy_post_process/src/auto_exposure/compensation_curve.rs @@ -193,11 +193,12 @@ impl RenderAsset for GpuAutoExposureCompensationCurve { } fn prepare_asset( - source: Self::SourceAsset, + source: AssetSnapshot, _: AssetId, (render_device, render_queue): &mut SystemParamItem, _: Option<&Self>, - ) -> Result> { + ) -> Result>> + { let texture = render_device.create_texture_with_data( render_queue, &TextureDescriptor { diff --git a/crates/bevy_render/src/erased_render_asset.rs b/crates/bevy_render/src/erased_render_asset.rs index 9c56ba4585829..5da82733b94dc 100644 --- a/crates/bevy_render/src/erased_render_asset.rs +++ b/crates/bevy_render/src/erased_render_asset.rs @@ -3,8 +3,8 @@ use crate::{ RenderSystems, Res, }; use bevy_app::{App, Plugin, SubApp}; -use bevy_asset::RenderAssetUsages; -use bevy_asset::{Asset, AssetEvent, AssetId, Assets, UntypedAssetId}; +use bevy_asset::{Asset, AssetEvent, AssetId, AssetSnapshot, Assets, UntypedAssetId}; +use bevy_asset::{AssetSnapshotStrategy, RenderAssetUsages}; use bevy_ecs::{ prelude::{Commands, IntoScheduleConfigs, MessageReader, ResMut, Resource}, schedule::{ScheduleConfigs, SystemSet}, @@ -38,7 +38,7 @@ pub struct AssetExtractionSystems; /// is transformed into its GPU-representation of type [`ErasedRenderAsset`]. pub trait ErasedRenderAsset: Send + Sync + 'static { /// The representation of the asset in the "main world". - type SourceAsset: Asset + Clone; + type SourceAsset: Asset>; /// The target representation of the asset in the "render world". type ErasedAsset: Send + Sync + 'static + Sized; @@ -68,10 +68,10 @@ pub trait ErasedRenderAsset: Send + Sync + 'static { /// /// ECS data may be accessed via `param`. fn prepare_asset( - source_asset: Self::SourceAsset, + source_asset: AssetSnapshot, asset_id: AssetId, param: &mut SystemParamItem, - ) -> Result>; + ) -> Result>>; /// Called whenever the [`ErasedRenderAsset::SourceAsset`] has been removed. /// @@ -161,7 +161,7 @@ pub struct ExtractedAssets { /// The assets extracted this frame. /// /// These are assets that were either added or modified this frame. - pub extracted: Vec<(AssetId, A::SourceAsset)>, + pub extracted: Vec<(AssetId, AssetSnapshot)>, /// IDs of the assets that were removed this frame. /// @@ -284,16 +284,14 @@ pub(crate) fn extract_erased_render_asset( let mut extracted_assets = Vec::new(); let mut added = >::default(); for id in needs_extracting.drain() { - if let Some(asset) = assets.get(id) { - let asset_usage = A::asset_usage(asset); + if let Some(mut asset_entry) = assets.entry(id) { + let asset_usage = A::asset_usage(&*asset_entry.as_ref()); if asset_usage.contains(RenderAssetUsages::RENDER_WORLD) { if asset_usage == RenderAssetUsages::RENDER_WORLD { - if let Some(asset) = assets.remove(id) { - extracted_assets.push((id, asset)); - added.insert(id); - } + extracted_assets.push((id, asset_entry.snapshot_erased())); + added.insert(id); } else { - extracted_assets.push((id, asset.clone())); + extracted_assets.push((id, asset_entry.snapshot())); added.insert(id); } } @@ -315,7 +313,7 @@ pub(crate) fn extract_erased_render_asset( /// All assets that should be prepared next frame. #[derive(Resource)] pub struct PrepareNextFrameAssets { - assets: Vec<(AssetId, A::SourceAsset)>, + assets: Vec<(AssetId, AssetSnapshot)>, } impl Default for PrepareNextFrameAssets { diff --git a/crates/bevy_render/src/mesh/mod.rs b/crates/bevy_render/src/mesh/mod.rs index 2c2f84a0a9e66..a2b46f7ffbdd6 100644 --- a/crates/bevy_render/src/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mod.rs @@ -6,7 +6,7 @@ use crate::{ }; use allocator::MeshAllocatorPlugin; use bevy_app::{App, Plugin}; -use bevy_asset::{AssetId, RenderAssetUsages}; +use bevy_asset::{AssetId, AssetSnapshot, RenderAssetUsages}; use bevy_ecs::{ prelude::*, system::{ @@ -145,11 +145,11 @@ impl RenderAsset for RenderMesh { /// Converts the extracted mesh into a [`RenderMesh`]. fn prepare_asset( - mesh: Self::SourceAsset, + mesh: AssetSnapshot, _: AssetId, (_images, mesh_vertex_buffer_layouts): &mut SystemParamItem, _: Option<&Self>, - ) -> Result> { + ) -> Result>> { #[cfg(feature = "morph")] let morph_targets = match mesh.morph_targets() { Some(mt) => { diff --git a/crates/bevy_render/src/render_asset.rs b/crates/bevy_render/src/render_asset.rs index 894ad7b65269a..5a81802fcbe3c 100644 --- a/crates/bevy_render/src/render_asset.rs +++ b/crates/bevy_render/src/render_asset.rs @@ -3,7 +3,9 @@ use crate::{ RenderSystems, Res, }; use bevy_app::{App, Plugin, SubApp}; -use bevy_asset::{Asset, AssetEvent, AssetId, Assets, RenderAssetUsages}; +use bevy_asset::{ + Asset, AssetEvent, AssetId, AssetSnapshot, AssetSnapshotStrategy, Assets, RenderAssetUsages, +}; use bevy_ecs::{ prelude::{Commands, IntoScheduleConfigs, MessageReader, ResMut, Resource}, schedule::{ScheduleConfigs, SystemSet}, @@ -37,7 +39,7 @@ pub struct AssetExtractionSystems; /// is transformed into its GPU-representation of type [`RenderAsset`]. pub trait RenderAsset: Send + Sync + 'static + Sized { /// The representation of the asset in the "main world". - type SourceAsset: Asset + Clone; + type SourceAsset: Asset> + Clone; /// Specifies all ECS data required by [`RenderAsset::prepare_asset`]. /// @@ -65,11 +67,11 @@ pub trait RenderAsset: Send + Sync + 'static + Sized { /// /// ECS data may be accessed via `param`. fn prepare_asset( - source_asset: Self::SourceAsset, + source_asset: AssetSnapshot, asset_id: AssetId, param: &mut SystemParamItem, previous_asset: Option<&Self>, - ) -> Result>; + ) -> Result>>; /// Called whenever the [`RenderAsset::SourceAsset`] has been removed. /// @@ -153,7 +155,7 @@ pub struct ExtractedAssets { /// The assets extracted this frame. /// /// These are assets that were either added or modified this frame. - pub extracted: Vec<(AssetId, A::SourceAsset)>, + pub extracted: Vec<(AssetId, AssetSnapshot)>, /// IDs of the assets that were removed this frame. /// @@ -276,16 +278,14 @@ pub(crate) fn extract_render_asset( let mut extracted_assets = Vec::new(); let mut added = >::default(); for id in needs_extracting.drain() { - if let Some(asset) = assets.get(id) { - let asset_usage = A::asset_usage(asset); + if let Some(mut asset_entry) = assets.entry(id) { + let asset_usage = A::asset_usage(&*asset_entry.as_ref()); if asset_usage.contains(RenderAssetUsages::RENDER_WORLD) { if asset_usage == RenderAssetUsages::RENDER_WORLD { - if let Some(asset) = assets.remove(id) { - extracted_assets.push((id, asset)); - added.insert(id); - } + extracted_assets.push((id, asset_entry.snapshot_erased())); + added.insert(id); } else { - extracted_assets.push((id, asset.clone())); + extracted_assets.push((id, asset_entry.snapshot())); added.insert(id); } } @@ -307,7 +307,7 @@ pub(crate) fn extract_render_asset( /// All assets that should be prepared next frame. #[derive(Resource)] pub struct PrepareNextFrameAssets { - assets: Vec<(AssetId, A::SourceAsset)>, + assets: Vec<(AssetId, AssetSnapshot)>, } impl Default for PrepareNextFrameAssets { diff --git a/crates/bevy_render/src/storage.rs b/crates/bevy_render/src/storage.rs index d7c208f001bf3..3d0e899172fa8 100644 --- a/crates/bevy_render/src/storage.rs +++ b/crates/bevy_render/src/storage.rs @@ -4,7 +4,7 @@ use crate::{ renderer::RenderDevice, }; use bevy_app::{App, Plugin}; -use bevy_asset::{Asset, AssetApp, AssetId, RenderAssetUsages}; +use bevy_asset::{Asset, AssetApp, AssetId, AssetSnapshot, RenderAssetUsages}; use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_utils::default; @@ -112,16 +112,16 @@ impl RenderAsset for GpuShaderStorageBuffer { } fn prepare_asset( - source_asset: Self::SourceAsset, + source_asset: AssetSnapshot, _: AssetId, render_device: &mut SystemParamItem, _: Option<&Self>, - ) -> Result> { - match source_asset.data { + ) -> Result>> { + match &source_asset.data { Some(data) => { let buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { label: source_asset.buffer_description.label, - contents: &data, + contents: data, usage: source_asset.buffer_description.usage, }); Ok(GpuShaderStorageBuffer { buffer }) diff --git a/crates/bevy_render/src/texture/gpu_image.rs b/crates/bevy_render/src/texture/gpu_image.rs index 622ffd16fb156..d3437ecf6d9e7 100644 --- a/crates/bevy_render/src/texture/gpu_image.rs +++ b/crates/bevy_render/src/texture/gpu_image.rs @@ -3,7 +3,7 @@ use crate::{ render_resource::{DefaultImageSampler, Sampler, Texture, TextureView}, renderer::{RenderDevice, RenderQueue}, }; -use bevy_asset::{AssetId, RenderAssetUsages}; +use bevy_asset::{AssetId, AssetSnapshot, RenderAssetUsages}; use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem}; use bevy_image::{Image, ImageSampler}; use bevy_math::{AspectRatio, UVec2}; @@ -42,11 +42,11 @@ impl RenderAsset for GpuImage { /// Converts the extracted image into a [`GpuImage`]. fn prepare_asset( - image: Self::SourceAsset, + image: AssetSnapshot, _: AssetId, (render_device, render_queue, default_sampler): &mut SystemParamItem, previous_asset: Option<&Self>, - ) -> Result> { + ) -> Result>> { let texture = if let Some(ref data) = image.data { render_device.create_texture_with_data( render_queue, @@ -89,14 +89,13 @@ impl RenderAsset for GpuImage { new_texture }; - let texture_view = texture.create_view( - image - .texture_view_descriptor - .or_else(|| Some(TextureViewDescriptor::default())) - .as_ref() - .unwrap(), - ); - let sampler = match image.sampler { + let texture_view = if let Some(descriptor) = image.texture_view_descriptor.as_ref() { + texture.create_view(descriptor) + } else { + texture.create_view(&TextureViewDescriptor::default()) + }; + + let sampler = match &image.sampler { ImageSampler::Default => (***default_sampler).clone(), ImageSampler::Descriptor(descriptor) => { render_device.create_sampler(&descriptor.as_wgpu()) diff --git a/crates/bevy_sprite_render/src/mesh2d/material.rs b/crates/bevy_sprite_render/src/mesh2d/material.rs index d4f64fa87acc0..bc5991fcc3b96 100644 --- a/crates/bevy_sprite_render/src/mesh2d/material.rs +++ b/crates/bevy_sprite_render/src/mesh2d/material.rs @@ -5,7 +5,10 @@ use crate::{ }; use bevy_app::{App, Plugin, PostUpdate}; use bevy_asset::prelude::AssetChanged; -use bevy_asset::{AsAssetId, Asset, AssetApp, AssetEventSystems, AssetId, AssetServer, Handle}; +use bevy_asset::{ + AsAssetId, Asset, AssetApp, AssetEventSystems, AssetId, AssetServer, AssetSnapshot, + AssetSnapshotStrategy, Handle, +}; use bevy_camera::visibility::ViewVisibility; use bevy_core_pipeline::{ core_2d::{ @@ -128,7 +131,9 @@ pub const MATERIAL_2D_BIND_GROUP_INDEX: usize = 2; /// @group(2) @binding(1) var color_texture: texture_2d; /// @group(2) @binding(2) var color_sampler: sampler; /// ``` -pub trait Material2d: AsBindGroup + Asset + Clone + Sized { +pub trait Material2d: + AsBindGroup + Asset> + Clone + Sized +{ /// Returns this material's vertex shader. If [`ShaderRef::Default`] is returned, the default mesh vertex shader /// will be used. fn vertex_shader() -> ShaderRef { @@ -972,7 +977,7 @@ impl RenderAsset for PreparedMaterial2d { ); fn prepare_asset( - material: Self::SourceAsset, + material: AssetSnapshot, _: AssetId, ( render_device, @@ -984,7 +989,7 @@ impl RenderAsset for PreparedMaterial2d { material_param, ): &mut SystemParamItem, _: Option<&Self>, - ) -> Result> { + ) -> Result>> { let bind_group_data = material.bind_group_data(); match material.as_bind_group( &pipeline.material2d_layout, diff --git a/crates/bevy_sprite_render/src/mesh2d/wireframe2d.rs b/crates/bevy_sprite_render/src/mesh2d/wireframe2d.rs index 5f9a0a0e6e54d..84c277e0bebe6 100644 --- a/crates/bevy_sprite_render/src/mesh2d/wireframe2d.rs +++ b/crates/bevy_sprite_render/src/mesh2d/wireframe2d.rs @@ -5,7 +5,7 @@ use crate::{ use bevy_app::{App, Plugin, PostUpdate, Startup, Update}; use bevy_asset::{ embedded_asset, load_embedded_asset, prelude::AssetChanged, AsAssetId, Asset, AssetApp, - AssetEventSystems, AssetId, AssetServer, Assets, Handle, UntypedAssetId, + AssetEventSystems, AssetId, AssetServer, AssetSnapshot, Assets, Handle, UntypedAssetId, }; use bevy_camera::{visibility::ViewVisibility, Camera, Camera2d}; use bevy_color::{Color, ColorToComponents}; @@ -472,11 +472,11 @@ impl RenderAsset for RenderWireframeMaterial { type Param = (); fn prepare_asset( - source_asset: Self::SourceAsset, + source_asset: AssetSnapshot, _asset_id: AssetId, _param: &mut SystemParamItem, _previous_asset: Option<&Self>, - ) -> Result> { + ) -> Result>> { Ok(RenderWireframeMaterial { color: source_asset.color.to_linear().to_f32_array(), }) diff --git a/crates/bevy_ui_render/src/ui_material.rs b/crates/bevy_ui_render/src/ui_material.rs index fc3ff836bdc40..da7a6dc9159ba 100644 --- a/crates/bevy_ui_render/src/ui_material.rs +++ b/crates/bevy_ui_render/src/ui_material.rs @@ -1,5 +1,5 @@ use crate::Node; -use bevy_asset::{Asset, AssetId, Handle}; +use bevy_asset::{Asset, AssetId, AssetSnapshotStrategy, Handle}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{component::Component, reflect::ReflectComponent}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; @@ -99,7 +99,9 @@ use derive_more::derive::From; /// /// } /// ``` -pub trait UiMaterial: AsBindGroup + Asset + Clone + Sized { +pub trait UiMaterial: + AsBindGroup + Asset> + Clone + Sized +{ /// Returns this materials vertex shader. If [`ShaderRef::Default`] is returned, the default UI /// vertex shader will be used. fn vertex_shader() -> ShaderRef { diff --git a/crates/bevy_ui_render/src/ui_material_pipeline.rs b/crates/bevy_ui_render/src/ui_material_pipeline.rs index 7c32b90918805..0cc63a642a86c 100644 --- a/crates/bevy_ui_render/src/ui_material_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_material_pipeline.rs @@ -557,13 +557,13 @@ impl RenderAsset for PreparedUiMaterial { ); fn prepare_asset( - material: Self::SourceAsset, + material: AssetSnapshot, _: AssetId, (render_device, pipeline_cache, pipeline, material_param): &mut SystemParamItem< Self::Param, >, _: Option<&Self>, - ) -> Result> { + ) -> Result>> { let bind_group_data = material.bind_group_data(); match material.as_bind_group( &pipeline.ui_layout.clone(), diff --git a/examples/asset/asset_decompression.rs b/examples/asset/asset_decompression.rs index ce24bc16c1993..a872cbc610bdc 100644 --- a/examples/asset/asset_decompression.rs +++ b/examples/asset/asset_decompression.rs @@ -3,7 +3,7 @@ use bevy::{ asset::{ io::{Reader, VecReader}, - AssetLoader, ErasedLoadedAsset, LoadContext, LoadDirectError, + Asset, AssetLoader, AssetStorageStrategy, ErasedLoadedAsset, LoadContext, LoadDirectError, }, prelude::*, reflect::TypePath, @@ -122,11 +122,15 @@ fn decompress>, A: Asset>( query: Query<(Entity, &Compressed)>, ) { for (entity, Compressed { compressed, .. }) in query.iter() { - let Some(GzAsset { uncompressed }) = compressed_assets.remove(compressed) else { + let Some(stored_gz_asset) = compressed_assets.remove(compressed) else { continue; }; - let uncompressed = uncompressed.take::().unwrap(); + let Some(gz_asset) = ::AssetStorage::into_inner(stored_gz_asset) else { + continue; + }; + + let uncompressed = gz_asset.uncompressed.take::().unwrap(); commands .entity(entity) diff --git a/examples/shader_advanced/manual_material.rs b/examples/shader_advanced/manual_material.rs index f7ac7c66cdec7..079b767c4cbb4 100644 --- a/examples/shader_advanced/manual_material.rs +++ b/examples/shader_advanced/manual_material.rs @@ -146,7 +146,7 @@ impl ErasedRenderAsset for ImageMaterial { ); fn prepare_asset( - source_asset: Self::SourceAsset, + source_asset: AssetSnapshot, asset_id: AssetId, ( opaque_draw_functions, @@ -157,7 +157,8 @@ impl ErasedRenderAsset for ImageMaterial { gpu_images, image_material_sampler, ): &mut SystemParamItem, - ) -> std::result::Result> { + ) -> std::result::Result>> + { let material_layout = material_layout.0.clone(); let draw_function_id = opaque_draw_functions.read().id::(); let bind_group_allocator = bind_group_allocators From bb1a7de4c3070f1651f9a39d1cda2e46386d51df Mon Sep 17 00:00:00 2001 From: Brian Reavis Date: Tue, 2 Dec 2025 16:52:58 -0800 Subject: [PATCH 2/6] Misc fixes --- crates/bevy_asset/src/asset_changed.rs | 2 +- crates/bevy_asset/src/lib.rs | 8 +-- crates/bevy_asset/src/reflect.rs | 84 +++++++++++++++++++++----- 3 files changed, 75 insertions(+), 19 deletions(-) diff --git a/crates/bevy_asset/src/asset_changed.rs b/crates/bevy_asset/src/asset_changed.rs index d3c8c05df81d9..fdf8b53f8d622 100644 --- a/crates/bevy_asset/src/asset_changed.rs +++ b/crates/bevy_asset/src/asset_changed.rs @@ -361,7 +361,7 @@ mod tests { .iter() .find_map(|(h, a)| (a.0 == i).then_some(h)) .unwrap(); - let mut asset = assets.get_mut(id).unwrap(); + let asset = assets.get_mut(id).unwrap(); println!("setting new value for {}", asset.0); asset.1 = "new_value"; }; diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 44b647ec271cc..719b60dbc7338 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -722,8 +722,8 @@ mod tests { }, loader::{AssetLoader, LoadContext}, Asset, AssetApp, AssetEvent, AssetId, AssetLoadError, AssetLoadFailedEvent, AssetPath, - AssetPlugin, AssetServer, Assets, InvalidGenerationError, LoadState, UnapprovedPathMode, - UntypedHandle, + AssetPlugin, AssetRef, AssetServer, Assets, InvalidGenerationError, LoadState, + UnapprovedPathMode, UntypedHandle, }; use alloc::{ boxed::Box, @@ -933,7 +933,7 @@ mod tests { const LARGE_ITERATION_COUNT: usize = 10000; - fn get(world: &World, id: AssetId) -> Option<&A> { + fn get<'w, A: Asset>(world: &'w World, id: AssetId) -> Option> { world.resource::>().get(id) } @@ -1199,7 +1199,7 @@ mod tests { { let mut texts = app.world_mut().resource_mut::>(); - let mut a = texts.get_mut(a_id).unwrap(); + let a = texts.get_mut(a_id).unwrap(); a.text = "Changed".to_string(); } diff --git a/crates/bevy_asset/src/reflect.rs b/crates/bevy_asset/src/reflect.rs index 8b50c0f29e953..1a856648c6b45 100644 --- a/crates/bevy_asset/src/reflect.rs +++ b/crates/bevy_asset/src/reflect.rs @@ -1,6 +1,7 @@ use alloc::boxed::Box; use core::{ any::{Any, TypeId}, + fmt::Debug, ops::{Deref, DerefMut}, }; @@ -44,6 +45,26 @@ impl<'a> AssetRefReflect<'a> { pub fn reflect(&mut self) -> ReflectRef<'_> { self.asset_ref.as_ref().reflect_ref() } + + /// Attempts to downcast the asset reference to a concrete type. + /// + /// Returns `Some(&T)` if the asset is of type `T`, or `None` otherwise. + #[inline] + pub fn downcast_ref(&self) -> Option<&T> { + self.asset_ref.as_ref().as_any().downcast_ref::() + } + + /// Returns the asset as `&dyn Any`. + #[inline] + pub fn as_any(&self) -> &dyn Any { + self.asset_ref.as_ref().as_any() + } +} + +impl<'a> Debug for AssetRefReflect<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.asset_ref.as_ref().fmt(f) + } } /// The equivalent of `&mut dyn Reflect`. @@ -89,6 +110,40 @@ impl<'a> AssetMutReflect<'a> { pub fn reflect_mut(&mut self) -> ReflectMut<'_> { self.asset_mut.as_mut().reflect_mut() } + + /// Attempts to downcast the asset reference to a concrete type. + /// + /// Returns `Some(&T)` if the asset is of type `T`, or `None` otherwise. + #[inline] + pub fn downcast_ref(&self) -> Option<&T> { + self.asset_mut.as_ref().as_any().downcast_ref::() + } + + /// Attempts to downcast the asset mutable reference to a concrete type. + /// + /// Returns `Some(&mut T)` if the asset is of type `T`, or `None` otherwise. + #[inline] + pub fn downcast_mut(&mut self) -> Option<&mut T> { + self.asset_mut.as_mut().as_any_mut().downcast_mut::() + } + + /// Returns the asset as `&dyn Any`. + #[inline] + pub fn as_any(&self) -> &dyn Any { + self.asset_mut.as_ref().as_any() + } + + /// Returns the asset as `&mut dyn Any`. + #[inline] + pub fn as_any_mut(&mut self) -> &mut dyn Any { + self.asset_mut.as_mut().as_any_mut() + } +} + +impl<'a> Debug for AssetMutReflect<'a> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + self.asset_mut.as_ref().fmt(f) + } } /// Type data for the [`TypeRegistry`](bevy_reflect::TypeRegistry) used to operate on reflected [`Asset`]s. @@ -382,27 +437,28 @@ mod tests { }; let handle = reflect_asset.add(app.world_mut(), &value); - // struct is a reserved keyword, so we can't use it here - let strukt = reflect_asset - .get_mut(app.world_mut(), &handle) - .unwrap() - .reflect_mut() - .as_struct() - .unwrap(); - strukt - .field_mut("field") - .unwrap() - .apply(&String::from("edited")); + + { + let mut reflect_mut = reflect_asset.get_mut(app.world_mut(), &handle).unwrap(); + // struct is a reserved keyword, so we can't use it here + let strukt = reflect_mut.reflect_mut().as_struct().unwrap(); + strukt + .field_mut("field") + .unwrap() + .apply(&String::from("edited")) + }; assert_eq!(reflect_asset.len(app.world()), 1); let ids: Vec<_> = reflect_asset.ids(app.world()).collect(); assert_eq!(ids.len(), 1); let id = ids[0]; - let asset = reflect_asset.get(app.world(), id).unwrap(); - assert_eq!(asset.downcast_ref::().unwrap().field, "edited"); + { + let asset = reflect_asset.get(app.world(), id).unwrap(); + assert_eq!(asset.downcast_ref::().unwrap().field, "edited"); + } - reflect_asset.remove(app.world_mut(), id).unwrap(); + reflect_asset.remove(app.world_mut(), id); assert_eq!(reflect_asset.len(app.world()), 0); } } From 315ef2f8daa36810482fc397607cf381c025d749 Mon Sep 17 00:00:00 2001 From: Brian Reavis Date: Tue, 2 Dec 2025 17:00:05 -0800 Subject: [PATCH 3/6] Unwrap remove_untracked result --- crates/bevy_pbr/src/meshlet/meshlet_mesh_manager.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/bevy_pbr/src/meshlet/meshlet_mesh_manager.rs b/crates/bevy_pbr/src/meshlet/meshlet_mesh_manager.rs index 29d7a75d57fd3..808e8e42da2de 100644 --- a/crates/bevy_pbr/src/meshlet/meshlet_mesh_manager.rs +++ b/crates/bevy_pbr/src/meshlet/meshlet_mesh_manager.rs @@ -2,7 +2,7 @@ use crate::meshlet::asset::{BvhNode, MeshletAabb, MeshletCullData}; use super::{asset::Meshlet, persistent_buffer::PersistentGpuBuffer, MeshletMesh}; use alloc::sync::Arc; -use bevy_asset::{AssetId, Assets}; +use bevy_asset::{AssetId, AssetStorageStrategy, Assets}; use bevy_ecs::{ resource::Resource, system::{Commands, Res, ResMut}, @@ -50,7 +50,9 @@ impl MeshletMeshManager { assets: &mut Assets, ) -> (u32, MeshletAabb, u32) { let queue_meshlet_mesh = |asset_id: &AssetId| { - let meshlet_mesh = assets.remove_untracked(*asset_id).expect( + let meshlet_mesh = assets.remove_untracked(*asset_id).and_then(|stored_asset| { + ::AssetStorage::into_inner(stored_asset) + }).expect( "MeshletMesh asset was already unloaded but is not registered with MeshletMeshManager", ); From 2814636dab16116d608af95ce9c5afa766e0e968 Mon Sep 17 00:00:00 2001 From: Brian Reavis Date: Tue, 2 Dec 2025 17:11:48 -0800 Subject: [PATCH 4/6] Fix missing import --- crates/bevy_pbr/src/meshlet/meshlet_mesh_manager.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_pbr/src/meshlet/meshlet_mesh_manager.rs b/crates/bevy_pbr/src/meshlet/meshlet_mesh_manager.rs index 808e8e42da2de..45d39a6c90eec 100644 --- a/crates/bevy_pbr/src/meshlet/meshlet_mesh_manager.rs +++ b/crates/bevy_pbr/src/meshlet/meshlet_mesh_manager.rs @@ -2,7 +2,7 @@ use crate::meshlet::asset::{BvhNode, MeshletAabb, MeshletCullData}; use super::{asset::Meshlet, persistent_buffer::PersistentGpuBuffer, MeshletMesh}; use alloc::sync::Arc; -use bevy_asset::{AssetId, AssetStorageStrategy, Assets}; +use bevy_asset::{Asset, AssetId, AssetStorageStrategy, Assets}; use bevy_ecs::{ resource::Resource, system::{Commands, Res, ResMut}, From 77ca908183a4430d0edc9c31ef6b54a01700d30d Mon Sep 17 00:00:00 2001 From: Brian Reavis Date: Tue, 2 Dec 2025 17:19:16 -0800 Subject: [PATCH 5/6] Fix docs --- crates/bevy_asset/src/assets.rs | 13 ++++++------- crates/bevy_asset/src/reflect.rs | 4 ++-- crates/bevy_asset/src/storage/mod.rs | 4 ++-- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/crates/bevy_asset/src/assets.rs b/crates/bevy_asset/src/assets.rs index e210bd8fe3540..b3533a8d06b8f 100644 --- a/crates/bevy_asset/src/assets.rs +++ b/crates/bevy_asset/src/assets.rs @@ -307,7 +307,7 @@ where pub fn snapshot(&'a mut self) -> AssetSnapshot { A::AssetStorage::get_snapshot(self.stored_asset) } - /// Instead of returning a clone of the asset or an Arc clone like [`StoredAssetEntry::snapshot`], + /// Instead of returning a clone of the asset or an Arc clone like [`crate::StoredAssetEntry::snapshot`], /// this will take ownership of the asset and put the entry in [`Assets`] into an erased state. /// /// Future attempts to get the asset will fail. @@ -491,9 +491,7 @@ impl Assets { stored_asset.map(|stored_asset| A::AssetStorage::get_ref(stored_asset)) } - /// Returns a snapshot of the [`Asset`] with the given `id`, if it exists. For sometimes, this will be - /// a clone (memory copy), but if's a asset using [`ArcedStorageStrategy`], it will be a cheap arc clone. - #[inline] + /// Returns a snapshot of the asset, which is a clone of the asset `A` (or an `Arc` clone, depending on the storage strategy). pub fn get_snapshot(&mut self, id: impl Into>) -> Option> where A::AssetStorage: AssetSnapshotStrategy, @@ -505,9 +503,10 @@ impl Assets { stored_asset.map(|stored_asset| A::AssetStorage::get_snapshot(stored_asset)) } - /// Returns a snapshot of the [`Asset`] with the given `id`, if it exists. For sometimes, this will be - /// a clone (memory copy), but if's a asset using [`ArcedStorageStrategy`], it will be a cheap arc clone. - #[inline] + /// Instead of returning a clone of the asset or an Arc clone like [`crate::StoredAssetEntry::snapshot`], + /// this will take ownership of the asset and put the entry in [`Assets`] into an erased state. + /// + /// Future attempts to get the asset will fail. pub fn get_snapshot_erased(&mut self, id: impl Into>) -> Option> where A::AssetStorage: AssetSnapshotStrategy, diff --git a/crates/bevy_asset/src/reflect.rs b/crates/bevy_asset/src/reflect.rs index 1a856648c6b45..f01503dba031e 100644 --- a/crates/bevy_asset/src/reflect.rs +++ b/crates/bevy_asset/src/reflect.rs @@ -445,8 +445,8 @@ mod tests { strukt .field_mut("field") .unwrap() - .apply(&String::from("edited")) - }; + .apply(&String::from("edited")); + } assert_eq!(reflect_asset.len(app.world()), 1); let ids: Vec<_> = reflect_asset.ids(app.world()).collect(); diff --git a/crates/bevy_asset/src/storage/mod.rs b/crates/bevy_asset/src/storage/mod.rs index 49572dd6779bf..6a5e69f0246af 100644 --- a/crates/bevy_asset/src/storage/mod.rs +++ b/crates/bevy_asset/src/storage/mod.rs @@ -65,8 +65,8 @@ pub trait AssetSnapshotStrategy: AssetStorageStrategy { /// Returns a snapshot of the asset, which is a clone of the asset `A` (or an `Arc` clone, depending on the storage strategy). fn get_snapshot(stored_asset: &mut Self::AssetStorage) -> Self::AssetSnapshot; - /// Instead of returning a clone of the asset or an Arc clone like [`StoredAssetEntry::snapshot`], - /// this will take ownership of the asset and put the entry in [`Assets`] into an erased state. + /// Instead of returning a clone of the asset or an Arc clone like [`crate::StoredAssetEntry::snapshot`], + /// this will take ownership of the asset and put the entry in [`crate::Assets`] into an erased state. /// /// Future attempts to get the asset will fail. fn get_snapshot_erased(stored_asset: &mut Self::AssetStorage) -> Self::AssetSnapshot; From 754b63a7ba034c5237f25ac1b971e049a7323032 Mon Sep 17 00:00:00 2001 From: Brian Reavis Date: Tue, 2 Dec 2025 17:32:53 -0800 Subject: [PATCH 6/6] Fix docs --- crates/bevy_asset/src/storage/boxed.rs | 4 ++-- crates/bevy_asset/src/storage/hybrid.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_asset/src/storage/boxed.rs b/crates/bevy_asset/src/storage/boxed.rs index 95bebcc5b209e..391ecba8c90a7 100644 --- a/crates/bevy_asset/src/storage/boxed.rs +++ b/crates/bevy_asset/src/storage/boxed.rs @@ -11,9 +11,9 @@ macro_rules! panic_asset_erased { }; } -/// This storage strategy wraps assets in a [`Box`]. This is less preferable than [`StackAssetStorage`], +/// This storage strategy wraps assets in a [`Box`]. This is less preferable than [`crate::StackAssetStorage`], /// except for cases when the stack size of the asset type is large. Boxing reduces the performance cost -/// of resizing the inner storage of `Assets` when assets are added and the capacity is exceeded. +/// of resizing the inner storage of [`crate::Assets`] when assets are added and the capacity is exceeded. pub struct BoxedAssetStorage; impl AssetStorageStrategy for BoxedAssetStorage { diff --git a/crates/bevy_asset/src/storage/hybrid.rs b/crates/bevy_asset/src/storage/hybrid.rs index 388f07f4fe871..4a17291431bfe 100644 --- a/crates/bevy_asset/src/storage/hybrid.rs +++ b/crates/bevy_asset/src/storage/hybrid.rs @@ -19,7 +19,7 @@ macro_rules! panic_asset_erased { /// This storage strategy provides async read/write access to assets. /// -/// This is achieved by storing the asset on the stack by default, and upgrading to an `Arc` or `RwLock` when needed. +/// This is achieved by storing the asset on the stack by default, and upgrading to an [`Arc`] or [`RwLock`] when needed. /// This approach allows assets that *don't* need to be shared across threads to not have to pay the potential performance cost /// of locking. Also, for asset types that never request a `RwLock`, the compiler's optimizer should be able to optimize-away /// the lock completely.