diff --git a/_release-content/migration-guides/assets_as_entities.md b/_release-content/migration-guides/assets_as_entities.md new file mode 100644 index 0000000000000..b918562d6d38d --- /dev/null +++ b/_release-content/migration-guides/assets_as_entities.md @@ -0,0 +1,195 @@ +--- +title: Assets-as-entities +pull_requests: [22939] +--- + +Previously assets were stored in a resource named `Assets`. Assets are now stored as components on +entities in the ECS world. This has required us to redesign some of our APIs. Here are some common +cases and their replacements: + +## 1. Reading asset data + +- Replace `Res>` with `Assets`. + +Before: + +```rust +fn my_system(meshes: Res>) { + let handle = ...; + let data: &Mesh = meshes.get(&handle).unwrap(); +} +``` + +After: + +```rust +fn my_system(meshes: Assets) { + let handle = ...; + let data: &Mesh = meshes.get(&handle).unwrap(); +} +``` + +## 2. Mutating asset data + +- Replace `ResMut>` with `AssetsMut`. + +Before: + +```rust +fn my_system(mut meshes: ResMut>) { + let handle = ...; + let data: AssetMut = meshes.get_mut(&handle).unwrap(); +} +``` + +After: + +```rust +fn my_system(mut meshes: AssetsMut) { + let handle = ...; + let mut data: AssetMut = meshes.get_mut(&handle).unwrap(); +} +``` + +## 3. Adding new assets + +- Replace `ResMut>` with `AssetCommands`. + - For multiple asset types, you only need one `AssetCommands`. +- Replace `.add()` with `.spawn_asset()`. +- For types that previously implicitly converted to your type (e.g., `Cuboid` implements + `Into`), you must surround the value in `MyType::from`. + - If the asset type is "constrained" (e.g., you store the handle into `Handle`), you can + use `.into()` to convert your value instead. + +Before: + +```rust +fn my_system(mut meshes: ResMut>) { + // Cuboid gets implicitly converted to Mesh. + let handle = meshes.add(Cuboid::new(1.0, 2.0, 3.0)); +} +``` + +After: + +```rust +fn my_system(mut asset_commands: AssetCommands) { + let handle = asset_commands.spawn_asset(Mesh::from(Cuboid::new(1.0, 2.0, 3.0))); +} +``` + +Or: + +```rust +fn my_system(mut asset_commands: AssetCommands) { + let handle: Handle = asset_commands.spawn_asset(Cuboid::new(1.0, 2.0, 3.0).into()); +} +``` + +## 4. Removing assets + +- Replace `ResMut>` with `AssetCommands`. + - For multiple asset types, you only need one `AssetCommands`. +- Replace `.remove()` with `.remove_asset()`. + - **This does not return the asset**. To get the asset back, you can enqueue a command in + `Commands`, then use `world.remove_asset()`. + +Before: + +```rust +fn my_system(mut meshes: ResMut>) { + let handle = ...; + // We get back the data here. + let mesh = meshes.remove(&handle).unwrap(); +} +``` + +After: + +```rust +fn my_system(mut asset_commands: AssetCommands) { + let handle = ...; + // We don't get the data here, since this action is deferred. You need exclusive world access. + asset_commands.remove(&handle); +} +``` + +## 5. Spawning materials + +- Since we can no longer deduce the asset type (since we have an untyped `AssetCommands` and + `MeshMaterial3d` is generic), we need to explicitly convert colors to a material. Wrap your value + in `StandardMaterial::from` or `ColorMaterial::from` for 3D or 2D respectively. + +Before: + +```rust +fn my_system(mut commands: Commands, mut materials: ResMut>) { + commands.spawn(( + Mesh3d(...), + MeshMaterial3d(materials.add(Color::BLACK)), + )); +} +``` + +After: + +```rust +fn my_system(mut commands: Commands, mut asset_commands: AssetCommands) { + commands.spawn(( + Mesh3d(...), + MeshMaterial3d(asset_commands.spawn_asset(StandardMaterial::from(Color::BLACK))), + )); +} +``` + +## 6. UUID assets + +- Instead of accessing the `Assets` resource and inserting the asset into the handle, call + `world.spawn_uuid_asset()` or `app.world_mut().spawn_uuid_asset()`. + +Before: + +```rust +const IMAGE: Handle = uuid_handle!("1347c9b7-c46a-48e7-b7b8-023a354b7cac"); + +fn my_plugin(app: &mut App) { + app.world_mut().resource_mut::>().insert(&IMAGE, create_some_image()); +} +``` + +After: + +```rust +const IMAGE: Handle = uuid_handle!("1347c9b7-c46a-48e7-b7b8-023a354b7cac"); + +fn my_plugin(app: &mut App) { + app.world_mut().spawn_uuid_asset(IMAGE.uuid().unwrap(), create_some_image()); +} +``` + +## Dealing with "deferred" assets + +Some existing code may assume that adding an asset is instant - in other words, you can call +`Assets::add` and then `Assets::get_mut` to mutate that added asset in the same system. Now that +assets need to be spawned, calling `AssetCommands::spawn_asset` followed by `AssetsMut::get_mut` +does not allow you to mutably access the asset. This acts the same way how calling `Commands::spawn` +followed by `Query::get_mut` does not allow you to mutably access a component. + +There are several ways to deal with this. One possibility is to change your system to be exclusive, +spawning assets with `DirectAssetAccessExt::spawn_asset`, and mutating the asset with +`DirectAssetAccessExt::get_asset_mut`. Since these operate on a world, spawning the asset happens +immediately. + +Another possibility is to defer the spawning of assets until the end of your system: allow accessing +an asset either through `AssetsMut` or through a local "pending" collection. Create assets in this +pending collection instead of spawning them, and then at the end of your system, spawn all pending +assets. This approach won't work in all cases, but can be straight forward when possible! + +## Misc changes + +- `Assets::len` -> `Assets::count` +- `Assets::reserve_handle` -> `AssetCommands::reserve_handle` / `DirectAssetAccessExt::reserve_asset_handle`. +- `AssetServer::get_id_handle` -> `AssetServer::get_entity_handle` +- `ReflectAsset::assets_resource_type_id` -> `ReflectAsset::asset_data_type_id` +- `ReflectAsset::add` -> `ReflectAsset::spawn` +- `ReflectAsset::len` -> `ReflectAsset::count` diff --git a/_release-content/release-notes/assets_as_entities.md b/_release-content/release-notes/assets_as_entities.md new file mode 100644 index 0000000000000..555d047377777 --- /dev/null +++ b/_release-content/release-notes/assets_as_entities.md @@ -0,0 +1,18 @@ +--- +title: Assets-as-entities +authors: ["@andriyDev"] +pull_requests: [22939] +--- + +In previous versions of Bevy, assets were stored in a big `Assets` resource (per asset type). +Continuing our traditions, assets are now represented as entities! This allows us to remove some +bespoke implementations (like `AssetEvent`) and replace it with more generic ECS features (like +change detection or hooks/observers). We can now also take advantage of ECS features like +relationships in the implementation of assets. + +While the simplification of Bevy internals is nice, what's more interesting is how users can +**also** benefit from these ECS features. For example, users can attach arbitrary components to +entities, whether that be to extend an asset's data, or to "tag" assets for better observability in +their apps. While users can take advantage of these today, their usage is still limited (for +example, users cannot add arbitrary components from within loaders, only from the ECS). We expect +this API to evolve to bring even more ergonomic access and more features to users! diff --git a/benches/benches/bevy_render/extract_render_asset.rs b/benches/benches/bevy_render/extract_render_asset.rs index 350de956b4e75..8c269db6a50b7 100644 --- a/benches/benches/bevy_render/extract_render_asset.rs +++ b/benches/benches/bevy_render/extract_render_asset.rs @@ -1,5 +1,5 @@ use bevy_app::{App, AppLabel}; -use bevy_asset::{Asset, AssetApp, AssetEvent, AssetId, Assets, RenderAssetUsages}; +use bevy_asset::{Asset, AssetApp, AssetEvent, AssetId, DirectAssetAccessExt, RenderAssetUsages}; use bevy_ecs::prelude::*; use bevy_reflect::TypePath; use bevy_render::{ @@ -53,9 +53,8 @@ fn extract_render_asset_bench(c: &mut Criterion) { let mut handles = Vec::with_capacity(size); { - let mut assets = app.world_mut().resource_mut::>(); for _ in 0..size { - handles.push(assets.add(DummyAsset)); + handles.push(app.world_mut().spawn_asset(DummyAsset)); } } diff --git a/benches/benches/bevy_scene/spawn.rs b/benches/benches/bevy_scene/spawn.rs index 48efe6bf88989..6d7f2c9cbe861 100644 --- a/benches/benches/bevy_scene/spawn.rs +++ b/benches/benches/bevy_scene/spawn.rs @@ -10,7 +10,7 @@ use bevy_asset::{ memory::{Dir, MemoryAssetReader}, AssetSourceBuilder, AssetSourceId, }, - Asset, AssetApp, AssetLoader, AssetServer, Assets, Handle, + Asset, AssetApp, AssetLoader, AssetServer, DirectAssetAccessExt, Handle, }; use bevy_ecs::prelude::*; use bevy_scene::{prelude::*, ScenePatch}; @@ -49,15 +49,11 @@ fn spawn(c: &mut Criterion) { dir.insert_asset_text(Path::new("button.bsn"), ""); let asset_server = app.world().resource::().clone(); - let handle = asset_server.load("button.bsn"); + let handle: Handle = asset_server.load("button.bsn"); run_app_until(&mut app, || asset_server.is_loaded(&handle)); - let patch = app - .world() - .resource::>() - .get(&handle) - .unwrap(); + let patch = app.world().get_asset(handle.id()).unwrap(); assert!(patch.resolved.is_some()); b.iter(move || { diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs index bfeecb74803c3..37db608563c7a 100644 --- a/crates/bevy_animation/src/graph.rs +++ b/crates/bevy_animation/src/graph.rs @@ -12,12 +12,8 @@ use bevy_asset::{ }; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ - component::Component, - message::MessageReader, - reflect::ReflectComponent, - resource::Resource, - system::{Res, ResMut}, - template::FromTemplate, + component::Component, message::MessageReader, reflect::ReflectComponent, resource::Resource, + system::ResMut, template::FromTemplate, }; use bevy_platform::collections::HashMap; use bevy_reflect::{prelude::ReflectDefault, Reflect, TypePath}; @@ -851,7 +847,7 @@ pub struct NonPathHandleError; /// for quick evaluation of that graph's animations. pub(crate) fn thread_animation_graphs( mut threaded_animation_graphs: ResMut, - animation_graphs: Res>, + animation_graphs: Assets, mut animation_graph_asset_events: MessageReader>, ) { for animation_graph_asset_event in animation_graph_asset_events.read() { diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index defc52a7faf75..b9c7bb403bc83 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -38,7 +38,7 @@ use crate::{ }; use bevy_app::{AnimationSystems, App, Plugin, PostUpdate}; -use bevy_asset::{Asset, AssetApp, AssetEventSystems, Assets}; +use bevy_asset::{Asset, AssetApp, AssetData, AssetEventSystems, AssetSelfHandle, Assets}; use bevy_ecs::{prelude::*, resource::IsResource, world::EntityMutExcept}; use bevy_math::FloatOrd; use bevy_platform::{collections::HashMap, hash::NoOpHash}; @@ -988,8 +988,8 @@ impl AnimationPlayer { /// A system that triggers untargeted animation events for the currently-playing animations. fn trigger_untargeted_animation_events( mut commands: Commands, - clips: Res>, - graphs: Res>, + clips: Assets, + graphs: Assets, players: Query<(Entity, &AnimationPlayer, &AnimationGraphHandle)>, ) { for (entity, player, graph_id) in &players { @@ -1030,8 +1030,8 @@ fn trigger_untargeted_animation_events( /// A system that advances the time for all playing animations. pub fn advance_animations( time: Res { - pub(crate) fn insert(&mut self, asset_id: AssetId, tick: Tick) { + pub(crate) fn insert(&mut self, entity: AssetEntity, tick: Tick) { self.last_change_tick = tick; - self.change_ticks.insert(asset_id, tick); + self.change_ticks.insert(entity, tick); } - pub(crate) fn remove(&mut self, asset_id: &AssetId) { - self.change_ticks.remove(asset_id); + pub(crate) fn remove(&mut self, entity: &AssetEntity) { + self.change_ticks.remove(entity); } } @@ -50,6 +51,7 @@ impl Default for AssetChanges { Self { change_ticks: Default::default(), last_change_tick: Tick::new(0), + marker: PhantomData, } } } @@ -57,9 +59,12 @@ impl Default for AssetChanges { struct AssetChangeCheck<'w, A: AsAssetId> { // This should never be `None` in practice, but we need to handle the case // where the `AssetChanges` resource was removed. - change_ticks: Option<&'w HashMap, Tick>>, + change_ticks: Option<&'w HashMap>, + /// Mapping from UUID handles to entity handles. + uuid_map: Option<&'w AssetUuidMap>, last_run: Tick, this_run: Tick, + marker: PhantomData A>, } impl Clone for AssetChangeCheck<'_, A> { @@ -71,21 +76,43 @@ impl Clone for AssetChangeCheck<'_, A> { impl Copy for AssetChangeCheck<'_, A> {} impl<'w, A: AsAssetId> AssetChangeCheck<'w, A> { - fn new(changes: &'w AssetChanges, last_run: Tick, this_run: Tick) -> Self { + fn new( + changes: &'w AssetChanges, + uuid_map: Option<&'w AssetUuidMap>, + last_run: Tick, + this_run: Tick, + ) -> Self { Self { change_ticks: Some(&changes.change_ticks), + uuid_map, last_run, this_run, + marker: PhantomData, } } - // TODO(perf): some sort of caching? Each check has two levels of indirection, - // which is not optimal. + // TODO(perf): some sort of caching? Each check has (at least) two levels of indirection (more + // for UUID handles), which is not optimal. fn has_changed(&self, handle: &A) -> bool { let is_newer = |tick: &Tick| tick.is_newer_than(self.last_run, self.this_run); let id = handle.as_asset_id(); + // If the handle corresponds to the entity, just use that. If it's a UUID handle, try to + // resolve the entity using the UUID map. + // PERF: We could make this faster if we could lookup the type ID map once at the start and + // then just lookup the uuid here. + let entity = match id { + AssetId::Entity { entity, .. } => entity, + AssetId::Uuid { .. } => { + if let Some(entity) = self.uuid_map.and_then(|map| map.resolve_entity(id).ok()) { + entity + } else { + return false; + } + } + }; + self.change_ticks - .is_some_and(|change_ticks| change_ticks.get(&id).is_some_and(is_newer)) + .is_some_and(|change_ticks| change_ticks.get(&entity).is_some_and(is_newer)) } } @@ -93,11 +120,10 @@ impl<'w, A: AsAssetId> AssetChangeCheck<'w, A> { /// after the system last ran, where `A` is a component that implements /// [`AsAssetId`]. /// -/// Unlike `Changed`, this is true whenever the asset for the `A` -/// in `ResMut>` changed. For example, when a mesh changed through the -/// [`Assets::get_mut`] method, `AssetChanged` will iterate over all -/// entities with the `Handle` for that mesh. Meanwhile, `Changed>` -/// will iterate over no entities. +/// Unlike `Changed`, this is true whenever `A::Asset` in `AssetData` changed. For +/// example, when a mesh changes through the [`AssetsMut::get_mut`] method, +/// `AssetChanged` will iterate over all entities with the `Handle` for that mesh. +/// Meanwhile, `Changed>` will iterate over no entities. /// /// Swapping the actual `A` component is a common pattern. So you /// should check for _both_ `AssetChanged` and `Changed` with @@ -123,7 +149,7 @@ impl<'w, A: AsAssetId> AssetChangeCheck<'w, A> { /// If no `A` asset updated since the last time the system ran, then no lookups occur. /// /// [`AssetEventSystems`]: crate::AssetEventSystems -/// [`Assets::get_mut`]: crate::Assets::get_mut +/// [`AssetsMut::get_mut`]: crate::AssetsMut::get_mut pub struct AssetChanged(PhantomData); /// [`WorldQuery`] fetch for [`AssetChanged`]. @@ -147,6 +173,7 @@ impl<'w, A: AsAssetId> Clone for AssetChangedFetch<'w, A> { pub struct AssetChangedState { asset_id: ComponentId, resource_id: ComponentId, + uuid_map_id: ComponentId, _asset: PhantomData, } @@ -187,11 +214,21 @@ unsafe impl WorldQuery for AssetChanged { inner: None, check: AssetChangeCheck { change_ticks: None, + uuid_map: None, last_run, this_run, + marker: PhantomData, }, }; }; + // SAFETY: + // - `uuid_map_id` was obtained from the type ID of `AssetUuidMap`. + // - `update_component_access` added read access for `uuid_map_id`. + let uuid_map = unsafe { + world + .get_resource_by_id(state.uuid_map_id) + .map(|ptr| ptr.deref::()) + }; let has_updates = changes.last_change_tick.is_newer_than(last_run, this_run); AssetChangedFetch { @@ -200,7 +237,7 @@ unsafe impl WorldQuery for AssetChanged { unsafe { <&A>::init_fetch(world, &state.asset_id, last_run, this_run) }), - check: AssetChangeCheck::new(changes, last_run, this_run), + check: AssetChangeCheck::new(changes, uuid_map, last_run, this_run), } } @@ -246,24 +283,34 @@ unsafe impl WorldQuery for AssetChanged { component_access_set: &mut FilteredAccessSet, _world: UnsafeWorldCell, ) { - let mut filter = FilteredAccess::default(); - filter.add_read(state.resource_id); - filter.and_with(IS_RESOURCE); - - let conflicts = component_access_set.get_conflicts_single(&filter); - if conflicts.is_empty() { - component_access_set.add(filter); - return; + let mut resource_filter = FilteredAccess::default(); + resource_filter.add_read(state.resource_id); + resource_filter.and_with(IS_RESOURCE); + + let conflicts = component_access_set.get_conflicts_single(&resource_filter); + if !conflicts.is_empty() { + panic!("error[B0002]: AssetChanged<{}> in system {:?} conflicts with a previous system parameter. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", DebugName::type_name::(), system_name); } - panic!("error[B0002]: AssetChanged<{}> in system {:?} conflicts with a previous system parameter. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", DebugName::type_name::(), system_name); + component_access_set.add(resource_filter); + + let mut uuid_filter = FilteredAccess::default(); + uuid_filter.add_read(state.uuid_map_id); + uuid_filter.and_with(IS_RESOURCE); + let conflicts = component_access_set.get_conflicts_single(&uuid_filter); + if !conflicts.is_empty() { + panic!("error[B0002]: AssetUuidMap in system {:?} conflicts with a previous system parameter. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", system_name); + } + component_access_set.add(uuid_filter); } fn init_state(world: &mut World) -> AssetChangedState { let resource_id = world.init_resource::>(); let asset_id = world.register_component::(); + let uuid_map_id = world.init_resource::(); AssetChangedState { asset_id, resource_id, + uuid_map_id, _asset: PhantomData, } } @@ -271,9 +318,11 @@ unsafe impl WorldQuery for AssetChanged { fn get_state(components: &Components) -> Option { let resource_id = components.component_id::>()?; let asset_id = components.component_id::()?; + let uuid_map_id = components.component_id::()?; Some(AssetChangedState { asset_id, resource_id, + uuid_map_id, _asset: PhantomData, }) } @@ -311,21 +360,22 @@ unsafe impl QueryFilter for AssetChanged { #[cfg(test)] #[expect(clippy::print_stdout, reason = "Allowed in tests.")] mod tests { + use crate::direct_access_ext::AssetCommands; use crate::tests::create_app; - use crate::{AssetEventSystems, Handle}; + use crate::{AssetEventSystems, Assets, AssetsMut, Handle}; use alloc::{vec, vec::Vec}; use bevy_ecs::system::assert_is_system; use core::num::NonZero; use std::println; - use crate::{AssetApp, Assets}; + use crate::AssetApp; use bevy_app::{App, AppExit, PostUpdate, Startup, Update}; use bevy_ecs::schedule::IntoScheduleConfigs; use bevy_ecs::{ component::Component, message::MessageWriter, resource::Resource, - system::{Commands, IntoSystem, Local, Query, Res, ResMut}, + system::{Commands, IntoSystem, Local, Query, ResMut}, }; use bevy_reflect::TypePath; @@ -383,7 +433,7 @@ mod tests { fn count_update( mut counter: ResMut, - assets: Res>, + assets: Assets, query: Query<&MyComponent, AssetChanged>, ) { for handle in query.iter() { @@ -392,14 +442,14 @@ mod tests { } } - fn update_some(mut assets: ResMut>, mut run_count: Local) { + fn update_some(mut assets: AssetsMut, mut run_count: Local) { let mut update_index = |i| { let id = assets .iter() .find_map(|(h, a)| (a.0 == i).then_some(h)) .unwrap(); let mut asset = assets.get_mut(id).unwrap(); - println!("setting new value for {}", asset.0); + println!("setting new value for {}", (*asset).0); asset.1 = "new_value"; }; match *run_count { @@ -414,22 +464,18 @@ mod tests { *run_count += 1; } - fn add_some( - mut assets: ResMut>, - mut cmds: Commands, - mut run_count: Local, - ) { + fn add_some(mut asset_commands: AssetCommands, mut cmds: Commands, mut run_count: Local) { match *run_count { 1 => { - cmds.spawn(MyComponent(assets.add(MyAsset(0, "init")))); + cmds.spawn(MyComponent(asset_commands.spawn_asset(MyAsset(0, "init")))); } 0 | 2 => {} 3 => { - cmds.spawn(MyComponent(assets.add(MyAsset(1, "init")))); - cmds.spawn(MyComponent(assets.add(MyAsset(2, "init")))); + cmds.spawn(MyComponent(asset_commands.spawn_asset(MyAsset(1, "init")))); + cmds.spawn(MyComponent(asset_commands.spawn_asset(MyAsset(2, "init")))); } 4.. => { - cmds.spawn(MyComponent(assets.add(MyAsset(3, "init")))); + cmds.spawn(MyComponent(asset_commands.spawn_asset(MyAsset(3, "init")))); } }; *run_count += 1; @@ -470,9 +516,9 @@ mod tests { .insert_resource(Counter(vec![0, 0])) .add_systems( Startup, - |mut cmds: Commands, mut assets: ResMut>| { - let asset0 = assets.add(MyAsset(0, "init")); - let asset1 = assets.add(MyAsset(1, "init")); + |mut cmds: Commands, mut asset_commands: AssetCommands| { + let asset0 = asset_commands.spawn_asset(MyAsset(0, "init")); + let asset1 = asset_commands.spawn_asset(MyAsset(1, "init")); cmds.spawn(MyComponent(asset0.clone())); cmds.spawn(MyComponent(asset0)); cmds.spawn(MyComponent(asset1.clone())); diff --git a/crates/bevy_asset/src/assets.rs b/crates/bevy_asset/src/assets.rs index 6cc02309fc366..d1208ce0613f3 100644 --- a/crates/bevy_asset/src/assets.rs +++ b/crates/bevy_asset/src/assets.rs @@ -1,823 +1,469 @@ -use crate::asset_changed::AssetChanges; -use crate::{Asset, AssetEvent, AssetHandleProvider, AssetId, AssetServer, Handle, UntypedHandle}; -use alloc::{sync::Arc, vec::Vec}; +use crate::{ + asset_changed::AssetChanges, Asset, AssetEntity, AssetEvent, AssetEventUnusedWriters, + AssetHandleProvider, AssetId, AssetSelfHandle, AssetUuidMap, Handle, StrongHandle, + UntypedHandle, +}; +use alloc::sync::Arc; use bevy_ecs::{ + change_detection::{DetectChanges, DetectChangesMut}, + component::Component, + entity::Entity, + lifecycle::HookContext, message::MessageWriter, - resource::Resource, - system::{Res, ResMut, SystemChangeTick}, + query::Changed, + system::{Query, Res, ResMut, SystemChangeTick, SystemParam}, + world::{DeferredWorld, Mut, Ref, World}, +}; +use bevy_reflect::TypePath; +use core::{ + any::TypeId, + marker::PhantomData, + ops::{Deref, DerefMut}, }; -use bevy_platform::collections::HashMap; -use bevy_reflect::{Reflect, TypePath}; -use core::ops::{Deref, DerefMut}; -use core::{any::TypeId, iter::Enumerate, marker::PhantomData, sync::atomic::AtomicU32}; -use crossbeam_channel::{Receiver, Sender}; -use serde::{Deserialize, Serialize}; +use derive_more::{Deref, DerefMut}; use thiserror::Error; -use uuid::Uuid; - -/// 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( - Debug, Copy, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, Reflect, Serialize, Deserialize, -)] -pub struct AssetIndex { - pub(crate) generation: u32, - pub(crate) index: u32, -} - -impl AssetIndex { - /// Convert the [`AssetIndex`] into an opaque blob of bits to transport it in circumstances where carrying a strongly typed index isn't possible. - /// - /// The result of this function should not be relied upon for anything except putting it back into [`AssetIndex::from_bits`] to recover the index. - pub fn to_bits(self) -> u64 { - let Self { generation, index } = self; - ((generation as u64) << 32) | index as u64 - } - /// Convert an opaque `u64` acquired from [`AssetIndex::to_bits`] back into an [`AssetIndex`]. This should not be used with any inputs other than those - /// derived from [`AssetIndex::to_bits`], as there are no guarantees for what will happen with such inputs. - pub fn from_bits(bits: u64) -> Self { - let index = ((bits << 32) >> 32) as u32; - let generation = (bits >> 32) as u32; - Self { generation, index } - } -} - -/// Allocates generational [`AssetIndex`] values and facilitates their reuse. -pub(crate) struct AssetIndexAllocator { - /// A monotonically increasing index. - next_index: AtomicU32, - recycled_queue_sender: Sender, - /// This receives every recycled [`AssetIndex`]. It serves as a buffer/queue to store indices ready for reuse. - recycled_queue_receiver: Receiver, - recycled_sender: Sender, - recycled_receiver: Receiver, -} - -impl Default for AssetIndexAllocator { - fn default() -> Self { - let (recycled_queue_sender, recycled_queue_receiver) = crossbeam_channel::unbounded(); - let (recycled_sender, recycled_receiver) = crossbeam_channel::unbounded(); - Self { - recycled_queue_sender, - recycled_queue_receiver, - recycled_sender, - recycled_receiver, - next_index: Default::default(), - } - } -} - -impl AssetIndexAllocator { - /// Reserves a new [`AssetIndex`], either by reusing a recycled index (with an incremented generation), or by creating a new index - /// by incrementing the index counter for a given asset type `A`. - pub fn reserve(&self) -> AssetIndex { - if let Ok(mut recycled) = self.recycled_queue_receiver.try_recv() { - recycled.generation += 1; - self.recycled_sender.send(recycled).unwrap(); - recycled - } else { - AssetIndex { - index: self - .next_index - .fetch_add(1, core::sync::atomic::Ordering::Relaxed), - generation: 0, - } - } - } - /// Queues the given `index` for reuse. This should only be done if the `index` is no longer being used. - pub fn recycle(&self, index: AssetIndex) { - self.recycled_queue_sender.send(index).unwrap(); - } -} +/// Stores the actual data of an asset. +#[derive(Component, Default, Deref, DerefMut)] +#[component(on_add=write_added_asset_event::, on_remove=write_removed_asset_event::)] +pub struct AssetData(pub A); -/// A "loaded asset" containing the untyped handle for an asset stored in a given [`AssetPath`]. +/// [`SystemParam`] providing convenient access to assets using handles. /// -/// [`AssetPath`]: crate::AssetPath -#[derive(Asset, TypePath)] -pub struct LoadedUntypedAsset { - /// The handle to the loaded asset. - #[dependency] - pub handle: UntypedHandle, -} - -// PERF: do we actually need this to be an enum? Can we just use an "invalid" generation instead -#[derive(Default)] -enum Entry { - /// None is an indicator that this entry does not have live handles. - #[default] - None, - /// Some is an indicator that there is a live handle active for the entry at this [`AssetIndex`] - Some { value: Option, generation: u32 }, +/// While it is possible to access assets without this [`SystemParam`], this param resolves the +/// handles into entities for you. +/// +/// For mutable access, use [`AssetsMut`] instead. For creating new assets, see +/// [`DirectAssetAccessExt`](crate::DirectAssetAccessExt) or +/// [`AssetCommands`](crate::AssetCommands). +#[derive(SystemParam)] +pub struct Assets<'w, 's, A: Asset> { + /// Uuid map allowing us to resolve [`Handle::Uuid`] into entities that we can query for. + uuid_map: Res<'w, AssetUuidMap>, + /// The query for the actual asset data. + /// + /// Includes [`Entity`] to support iterating and returning [`AssetEntity`]. + assets: Query<'w, 's, (Entity, &'static AssetData)>, + /// The query for the self-handles of assets, allowing us to return an asset's strong handle. + handles: Query<'w, 's, &'static AssetSelfHandle>, } -/// Stores [`Asset`] values in a Vec-like storage identified by [`AssetIndex`]. -struct DenseAssetStorage { - storage: Vec>, - len: u32, - allocator: Arc, +/// [`SystemParam`] providing convenient access to assets using handles. +/// +/// While it is possible to access assets without this [`SystemParam`], this param resolves the +/// handles into entities for you. +/// +/// For only immutable access, use [`Assets`] instead. For creating new assets, see +/// [`DirectAssetAccessExt`](crate::DirectAssetAccessExt) or +/// [`AssetCommands`](crate::AssetCommands). +#[derive(SystemParam)] +pub struct AssetsMut<'w, 's, A: Asset> { + /// Uuid map allowing us to resolve [`Handle::Uuid`] into entities that we can query for. + uuid_map: Res<'w, AssetUuidMap>, + /// The query for the actual asset data. + /// + /// Includes [`Entity`] to support iterating and returning [`AssetEntity`]. + assets: Query<'w, 's, (Entity, &'static mut AssetData)>, + /// The query for the self-handles of assets, allowing us to return an asset's strong handle. + handles: Query<'w, 's, &'static AssetSelfHandle>, } -impl Default for DenseAssetStorage { - fn default() -> Self { - Self { - len: 0, - storage: Default::default(), - allocator: Default::default(), - } +impl Assets<'_, '_, A> { + /// Gets the data associated with the `handle` if it exists. + /// + /// This can return [`None`] for several reasons. For example, the asset could be despawned, + /// the asset may not currently have the asset data, or the UUID of the `id` may not resolve. + pub fn get(&self, id: impl Into>) -> Option<&A> { + let entity = self.uuid_map.resolve_entity(id.into()).ok()?; + self.assets + .get(entity.raw_entity()) + .ok() + .map(|(_, data)| &data.0) } -} -impl DenseAssetStorage { - // Returns the number of assets stored. - pub(crate) fn len(&self) -> usize { - self.len as usize - } - - // Returns `true` if there are no assets stored. - pub(crate) fn is_empty(&self) -> bool { - self.len == 0 - } - - /// Insert the value at the given index. Returns true if a value already exists (and was replaced) - pub(crate) fn insert( - &mut self, - index: AssetIndex, - asset: A, - ) -> Result { - self.flush(); - let entry = &mut self.storage[index.index as usize]; - if let Entry::Some { value, generation } = entry { - if *generation == index.generation { - let exists = value.is_some(); - if !exists { - self.len += 1; - } - *value = Some(asset); - Ok(exists) - } else { - Err(InvalidGenerationError::Occupied { - index, - current_generation: *generation, - }) - } - } else { - Err(InvalidGenerationError::Removed { index }) - } + /// Gets the strong handle for an entity. + /// + /// Handles are the primary "reference" for assets - most APIs will store handles. This also + /// allows you to keep the asset from being automatically despawned for as long as you hold the + /// handle. + /// + /// Returns [`None`] if the handles for this asset have already expired, meaning this asset is + /// queued for despawning. + pub fn get_strong_handle(&self, entity: AssetEntity) -> Option> { + let self_handle = self.handles.get(entity.raw_entity()).ok()?; + self_handle.upgrade().ok() } - /// 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 { - self.remove_internal(index, |dense_storage| { - dense_storage.storage[index.index as usize] = Entry::None; - dense_storage.allocator.recycle(index); - }) - } - - /// 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 { - self.remove_internal(index, |_| {}) - } - - fn remove_internal( - &mut self, - index: AssetIndex, - removed_action: impl FnOnce(&mut Self), - ) -> Option { - self.flush(); - let value = match &mut self.storage[index.index as usize] { - Entry::None => return None, - Entry::Some { value, generation } => { - if *generation == index.generation { - value.take().inspect(|_| self.len -= 1) - } else { - return None; - } - } + /// Returns `true` if the corresponding entity contains asset data, and `false` otherwise. + pub fn contains(&self, id: impl Into>) -> bool { + let Ok(entity) = self.uuid_map.resolve_entity(id.into()) else { + return false; }; - removed_action(self); - value - } - - pub(crate) fn get(&self, index: AssetIndex) -> Option<&A> { - let entry = self.storage.get(index.index as usize)?; - match entry { - Entry::None => None, - Entry::Some { value, generation } => { - if *generation == index.generation { - value.as_ref() - } else { - None - } - } - } - } - - pub(crate) fn get_mut(&mut self, index: AssetIndex) -> Option<&mut A> { - let entry = self.storage.get_mut(index.index as usize)?; - match entry { - Entry::None => None, - Entry::Some { value, generation } => { - if *generation == index.generation { - value.as_mut() - } else { - None - } - } - } + self.assets.contains(entity.raw_entity()) } - pub(crate) fn flush(&mut self) { - // NOTE: this assumes the allocator index is monotonically increasing. - let new_len = self - .allocator - .next_index - .load(core::sync::atomic::Ordering::Relaxed); - self.storage.resize_with(new_len as usize, || Entry::Some { - value: None, - generation: 0, - }); - while let Ok(recycled) = self.allocator.recycled_receiver.try_recv() { - let entry = &mut self.storage[recycled.index as usize]; - *entry = Entry::Some { - value: None, - generation: recycled.generation, - }; - } + /// Iterates through all assets. + pub fn iter(&self) -> impl Iterator { + self.assets + .iter() + .map(|(entity, data)| (AssetEntity::new_unchecked(entity), data.deref())) } - pub(crate) fn get_index_allocator(&self) -> Arc { - self.allocator.clone() + /// Returns `true` if there are no assets. + pub fn is_empty(&self) -> bool { + self.assets.is_empty() } - pub(crate) fn ids(&self) -> impl Iterator> + '_ { - self.storage - .iter() - .enumerate() - .filter_map(|(i, v)| match v { - Entry::None => None, - Entry::Some { value, generation } => { - if value.is_some() { - Some(AssetId::from(AssetIndex { - index: i as u32, - generation: *generation, - })) - } else { - None - } - } - }) + /// Returns the number of asset currently stored. + pub fn count(&self) -> usize { + self.assets.count() } } -/// 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 -/// the assets can only be identified at runtime. This is the default behavior. -/// -/// Assets identified by [`AssetId::Uuid`] will be stored in a hashmap. This is less efficient, but it means that the assets can be referenced -/// at compile time. -/// -/// This tracks (and queues) [`AssetEvent`] events whenever changes to the collection occur. -/// To check whether the asset used by a given component has changed (due to a change in the handle or the underlying asset) -/// use the [`AssetChanged`](crate::asset_changed::AssetChanged) query filter. -#[derive(Resource)] -pub struct Assets { - dense_storage: DenseAssetStorage, - hash_map: HashMap, - handle_provider: AssetHandleProvider, - queued_events: Vec>, - /// Assets managed by the `Assets` struct with live strong `Handle`s - /// originating from `get_strong_handle`. - duplicate_handles: HashMap, -} - -impl Default for Assets { - fn default() -> Self { - let dense_storage = DenseAssetStorage::default(); - let handle_provider = - AssetHandleProvider::new(TypeId::of::(), dense_storage.get_index_allocator()); +impl Clone for Assets<'_, '_, A> { + fn clone(&self) -> Self { Self { - dense_storage, - handle_provider, - hash_map: Default::default(), - queued_events: Default::default(), - duplicate_handles: Default::default(), + assets: self.assets, + handles: self.handles, + uuid_map: Res::clone(&self.uuid_map), } } } -impl Assets { - /// Retrieves an [`AssetHandleProvider`] capable of reserving new [`Handle`] values for assets that will be stored in this - /// collection. - pub fn get_handle_provider(&self) -> AssetHandleProvider { - self.handle_provider.clone() - } - - /// Reserves a new [`Handle`] for an asset that will be stored in this collection. - pub fn reserve_handle(&self) -> Handle { - self.handle_provider.reserve_handle().typed::() +impl<'s, A: Asset> AssetsMut<'_, 's, A> { + /// Gets the data associated with the `id` if it exists. + /// + /// This can return [`None`] for several reasons. For example, the asset could be despawned, + /// the asset may not currently have the asset data, or the UUID of the `id` may not resolve. + pub fn get(&self, id: impl Into>) -> Option<&A> { + let entity = self.uuid_map.resolve_entity(id.into()).ok()?; + self.assets + .get(entity.raw_entity()) + .ok() + .map(|(_, data)| &data.0) } - /// Inserts the given `asset`, identified by the given `id`. If an asset already exists for - /// `id`, it will be replaced. + /// Gets the data (mutably) associated with the `id` if it exists. /// - /// Note: This will never return an error for UUID asset IDs. - pub fn insert( - &mut self, - id: impl Into>, - asset: A, - ) -> Result<(), InvalidGenerationError> { - match id.into() { - AssetId::Index { index, .. } => self.insert_with_index(index, asset).map(|_| ()), - AssetId::Uuid { uuid } => { - self.insert_with_uuid(uuid, asset); - Ok(()) - } - } + /// This can return [`None`] for several reasons. For example, the asset could be despawned, + /// the asset may not currently have the asset data, or the UUID of the `id` may not resolve. + pub fn get_mut(&mut self, id: impl Into>) -> Option> { + let entity = self.uuid_map.resolve_entity(id.into()).ok()?; + self.assets + .get_mut(entity.raw_entity()) + .ok() + .map(|(_, data)| AssetMut(data)) } - /// Retrieves an [`Asset`] stored for the given `id` if it exists. If it does not exist, it will - /// be inserted using `insert_fn`. + /// Gets the strong handle for an entity. /// - /// 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, 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")) + /// Handles are the primary "reference" for assets - most APIs will store handles. This also + /// allows you to keep the asset from being automatically despawned for as long as you hold the + /// handle. + /// + /// Returns [`None`] if the handles for this asset have already expired, meaning this asset is + /// queued for despawning. + pub fn get_strong_handle(&self, entity: Entity) -> Option> { + let self_handle = self.handles.get(entity).ok()?; + self_handle.upgrade().ok() } - /// Returns `true` if the `id` exists in this collection. Otherwise it returns `false`. + /// Returns `true` if the corresponding entity contains asset data, and `false` otherwise. pub fn contains(&self, id: impl Into>) -> bool { - match id.into() { - AssetId::Index { index, .. } => self.dense_storage.get(index).is_some(), - AssetId::Uuid { uuid } => self.hash_map.contains_key(&uuid), - } + let Ok(entity) = self.uuid_map.resolve_entity(id.into()) else { + return false; + }; + self.assets.contains(entity.raw_entity()) } - pub(crate) fn insert_with_uuid(&mut self, uuid: Uuid, asset: A) -> Option { - let result = self.hash_map.insert(uuid, asset); - if result.is_some() { - self.queued_events - .push(AssetEvent::Modified { id: uuid.into() }); - } else { - self.queued_events - .push(AssetEvent::Added { id: uuid.into() }); - } - result - } - pub(crate) fn insert_with_index( - &mut self, - index: AssetIndex, - asset: A, - ) -> Result { - let replaced = self.dense_storage.insert(index, asset)?; - if replaced { - self.queued_events - .push(AssetEvent::Modified { id: index.into() }); - } else { - self.queued_events - .push(AssetEvent::Added { id: index.into() }); - } - Ok(replaced) + /// Iterates through all assets. + pub fn iter(&mut self) -> impl Iterator { + self.assets + .iter() + .map(|(entity, data)| (AssetEntity::new_unchecked(entity), data.deref())) } - /// Adds the given `asset` and allocates a new strong [`Handle`] for it. - #[inline] - pub fn add(&mut self, asset: impl Into) -> Handle { - let index = self.dense_storage.allocator.reserve(); - self.insert_with_index(index, asset.into()).unwrap(); - Handle::Strong(self.handle_provider.get_handle(index, false, None, None)) + /// Iterates through all assets mutably. + pub fn iter_mut(&mut self) -> impl Iterator)> { + self.assets + .iter_mut() + .map(|(entity, data)| (AssetEntity::new_unchecked(entity), AssetMut(data))) } - /// Upgrade an `AssetId` into a strong `Handle` that will prevent asset drop. - /// - /// Returns `None` if the provided `id` is not part of this `Assets` collection. - /// For example, it may have been dropped earlier. - #[inline] - pub fn get_strong_handle(&mut self, id: AssetId) -> Option> { - if !self.contains(id) { - return None; - } - let index = match id { - AssetId::Index { index, .. } => index, - // We don't support strong handles for Uuid assets. - AssetId::Uuid { .. } => return None, - }; - *self.duplicate_handles.entry(index).or_insert(0) += 1; - Some(Handle::Strong( - self.handle_provider.get_handle(index, false, None, None), - )) - } - - /// 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() { - AssetId::Index { index, .. } => self.dense_storage.get(index), - AssetId::Uuid { uuid } => self.hash_map.get(&uuid), - } + /// Returns `true` if there are no assets. + pub fn is_empty(&self) -> bool { + self.assets.is_empty() } - /// 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> { - let id: AssetId = id.into(); - let result = match id { - AssetId::Index { index, .. } => self.dense_storage.get_mut(index), - AssetId::Uuid { uuid } => self.hash_map.get_mut(&uuid), - }; - Some(AssetMut { - asset: result?, - guard: AssetMutChangeNotifier { - changed: false, - asset_id: id, - queued_events: &mut self.queued_events, - }, - }) + /// Returns the number of asset currently stored. + pub fn count(&self) -> usize { + self.assets.count() } - /// Retrieves a mutable reference to the [`Asset`] with the given `id`, if it exists. + /// Creates a readonly instance of this [`AssetsMut`]. /// - /// 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> { - let id: AssetId = id.into(); - match id { - AssetId::Index { index, .. } => self.dense_storage.get_mut(index), - AssetId::Uuid { uuid } => self.hash_map.get_mut(&uuid), + /// This allows calling code that only needs an [`Assets`]. + pub fn as_readonly<'w>(&'w self) -> Assets<'w, 's, A> { + Assets { + uuid_map: Res::clone(&self.uuid_map), + assets: self.assets.as_readonly(), + handles: self.handles.as_readonly(), } } +} - /// 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 { - let id: AssetId = id.into(); - let result = self.remove_untracked(id); - if result.is_some() { - self.queued_events.push(AssetEvent::Removed { id }); - } - result - } +/// Mutable access to asset data of type `A`. +/// +/// To acquite an instance, see [`AssetsMut::get_mut`] or [`AssetsMut::iter_mut`]. +/// +/// This mirrors the [`Mut`] type (providing mutable access to the asset component, as well as +/// change detection info), but hides the [`AssetData`] wrapper (the component actually storing the +/// asset data). +pub struct AssetMut<'w, A: Asset>(pub(crate) Mut<'w, AssetData>); - /// Removes (and returns) the [`Asset`] with the given `id`, if it exists. This skips emitting [`AssetEvent::Removed`]. - /// 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 { - let id: AssetId = id.into(); - match id { - AssetId::Index { index, .. } => self.dense_storage.remove_still_alive(index), - AssetId::Uuid { uuid } => self.hash_map.remove(&uuid), - } +impl<'w, A: Asset> AssetMut<'w, A> { + /// Consumes `self` and returns a mutable reference to the contained value while marking `self` + /// as "changed". + pub fn into_inner(self) -> &'w mut A { + self.0.into_inner() } +} - /// Removes the [`Asset`] with the given `id`. - pub(crate) fn remove_dropped(&mut self, index: AssetIndex) { - match self.duplicate_handles.get_mut(&index) { - None => {} - Some(0) => { - self.duplicate_handles.remove(&index); - } - Some(value) => { - *value -= 1; - return; - } - } +impl<'w, A: Asset> Deref for AssetMut<'w, A> { + type Target = A; - let existed = self.dense_storage.remove_dropped(index).is_some(); + fn deref(&self) -> &Self::Target { + self.0.deref() + } +} - self.queued_events - .push(AssetEvent::Unused { id: index.into() }); - if existed { - self.queued_events - .push(AssetEvent::Removed { id: index.into() }); - } +impl<'w, A: Asset> DerefMut for AssetMut<'w, A> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0.deref_mut().0 } +} - /// Returns `true` if there are no assets in this collection. - pub fn is_empty(&self) -> bool { - self.dense_storage.is_empty() && self.hash_map.is_empty() +impl<'w, A: Asset> DetectChanges for AssetMut<'w, A> { + fn is_added(&self) -> bool { + self.0.is_added() } - /// Returns the number of assets currently stored in the collection. - pub fn len(&self) -> usize { - self.dense_storage.len() + self.hash_map.len() + fn is_added_after(&self, other: bevy_ecs::change_detection::Tick) -> bool { + self.0.is_added_after(other) } - /// Returns an iterator over the [`AssetId`] of every [`Asset`] stored in this collection. - pub fn ids(&self) -> impl Iterator> + '_ { - self.dense_storage - .ids() - .chain(self.hash_map.keys().map(|uuid| AssetId::from(*uuid))) + fn is_changed(&self) -> bool { + self.0.is_changed() } - /// 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)> { - self.dense_storage - .storage - .iter() - .enumerate() - .filter_map(|(i, v)| match v { - Entry::None => None, - Entry::Some { value, generation } => value.as_ref().map(|v| { - let id = AssetId::Index { - index: AssetIndex { - generation: *generation, - index: i as u32, - }, - marker: PhantomData, - }; - (id, v) - }), - }) - .chain( - self.hash_map - .iter() - .map(|(i, v)| (AssetId::Uuid { uuid: *i }, v)), - ) - } - - /// Returns an iterator over the [`AssetId`] and mutable [`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_mut(&mut self) -> AssetsMutIterator<'_, A> { - AssetsMutIterator { - dense_storage: self.dense_storage.storage.iter_mut().enumerate(), - hash_map: self.hash_map.iter_mut(), - queued_events: &mut self.queued_events, - } + fn is_changed_after(&self, other: bevy_ecs::change_detection::Tick) -> bool { + self.0.is_changed_after(other) } - /// A system that synchronizes the state of assets in this collection with the [`AssetServer`]. This manages - /// [`Handle`] drop events. - pub fn track_assets(mut assets: ResMut, asset_server: Res) { - // note that we must hold this lock for the entire duration of this function to ensure - // that `asset_server.load` calls that occur during it block, which ensures that - // re-loads are kicked off appropriately. This function must be "transactional" relative - // to other asset info operations - let mut infos = asset_server.write_infos(); - while let Ok(drop_event) = assets.handle_provider.drop_receiver.try_recv() { - if drop_event.asset_server_managed { - // the process_handle_drop call checks whether new handles have been created since the drop event was fired, before removing the asset - if !infos.process_handle_drop(drop_event.index) { - // a new handle has been created, or the asset doesn't exist - continue; - } - } + fn last_changed(&self) -> bevy_ecs::change_detection::Tick { + self.0.last_changed() + } - assets.remove_dropped(drop_event.index.index); - } + fn added(&self) -> bevy_ecs::change_detection::Tick { + self.0.added() } - /// A system that applies accumulated asset change events to the [`Messages`] resource. - /// - /// [`Messages`]: bevy_ecs::message::Messages - pub(crate) fn asset_events( - mut assets: ResMut, - mut messages: MessageWriter>, - asset_changes: Option>>, - ticks: SystemChangeTick, - ) { - use AssetEvent::{Added, LoadedWithDependencies, Modified, Removed}; - - if let Some(mut asset_changes) = asset_changes { - for new_event in &assets.queued_events { - match new_event { - Removed { id } | AssetEvent::Unused { id } => asset_changes.remove(id), - Added { id } | Modified { id } | LoadedWithDependencies { id } => { - asset_changes.insert(*id, ticks.this_run()); - } - }; - } - } - messages.write_batch(assets.queued_events.drain(..)); + fn changed_by(&self) -> bevy_ecs::change_detection::MaybeLocation { + self.0.changed_by() } - /// A run condition for [`asset_events`]. The system will not run if there are no events to - /// flush. - /// - /// [`asset_events`]: Self::asset_events - pub(crate) fn asset_events_condition(assets: Res) -> bool { - !assets.queued_events.is_empty() + fn last_run(&self) -> bevy_ecs::change_detection::Tick { + self.0.last_run() } -} -/// Unique mutable borrow of an asset. -/// -/// [`AssetEvent::Modified`] events will be only triggered if an asset itself is mutably borrowed. -/// -/// Just as an example, this allows checking if a material property has changed -/// before modifying it to avoid unnecessary material extraction down the pipeline. -pub struct AssetMut<'a, A: Asset> { - asset: &'a mut A, - guard: AssetMutChangeNotifier<'a, A>, + fn this_run(&self) -> bevy_ecs::change_detection::Tick { + self.0.this_run() + } } -impl<'a, A: Asset> AssetMut<'a, A> { - /// Marks with inner asset as modified and returns reference to it. - pub fn into_inner(mut self) -> &'a mut A { - self.guard.changed = true; - self.asset - } +impl<'w, A: Asset> DetectChangesMut for AssetMut<'w, A> { + type Inner = A; - /// Returns reference to the inner asset but doesn't mark it as modified. - pub fn into_inner_untracked(self) -> &'a mut A { - self.asset + fn set_changed(&mut self) { + self.0.set_changed(); } - /// Manually bypasses change detection, allowing you to mutate the underlying value - /// without emitting [`AssetEvent::Modified`] event. - /// - /// # Warning - /// This is a risky operation, that can have unexpected consequences on any system relying on this code. - /// However, it can be an essential escape hatch when, for example, - /// you are trying to synchronize representations using change detection and need to avoid infinite recursion. - pub fn bypass_change_detection(&mut self) -> &mut A { - self.asset + fn set_added(&mut self) { + self.0.set_added(); } -} -impl<'a, A: Asset> Deref for AssetMut<'a, A> { - type Target = A; + fn set_last_changed(&mut self, last_changed: bevy_ecs::change_detection::Tick) { + self.0.set_last_changed(last_changed); + } - fn deref(&self) -> &Self::Target { - self.asset + fn set_last_added(&mut self, last_added: bevy_ecs::change_detection::Tick) { + self.0.set_last_added(last_added); } -} -impl<'a, A: Asset> DerefMut for AssetMut<'a, A> { - fn deref_mut(&mut self) -> &mut Self::Target { - self.guard.changed = true; - self.asset + fn bypass_change_detection(&mut self) -> &mut Self::Inner { + &mut self.0.bypass_change_detection().0 } } -/// Helper struct to allow safe destructuring of the [`AssetMut::into_inner`] -/// while also keeping strong change tracking guarantees. -struct AssetMutChangeNotifier<'a, A: Asset> { - changed: bool, - asset_id: AssetId, - queued_events: &'a mut Vec>, +/// A "loaded asset" containing the untyped handle for an asset stored in a given [`AssetPath`]. +/// +/// [`AssetPath`]: crate::AssetPath +#[derive(Asset, TypePath)] +pub struct LoadedUntypedAsset { + /// The handle to the loaded asset. + #[dependency] + pub handle: UntypedHandle, } -impl<'a, A: Asset> Drop for AssetMutChangeNotifier<'a, A> { - fn drop(&mut self) { - if self.changed { - self.queued_events - .push(AssetEvent::Modified { id: self.asset_id }); - } - } +/// Initializes the entity for `handle` with the minimum it needs to be considered an asset. +/// +/// Importantly, this omits inserting any asset data - use [`insert_asset`] for that. +pub(crate) fn setup_asset( + world: &mut World, + handle: &Arc, +) -> Result<(), AssetEntityDoesNotExistError> { + let Ok(mut entity) = world.get_entity_mut(handle.entity.raw_entity()) else { + return Err(AssetEntityDoesNotExistError(handle.entity)); + }; + entity.insert(AssetSelfHandle(Arc::downgrade(handle))); + Ok(()) } -/// A mutable iterator over [`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>, +/// Inserts [`AssetData`] holding `asset` onto `entity`. +pub(crate) fn insert_asset( + world: &mut World, + entity: AssetEntity, + asset: A, +) -> Result<(), AssetEntityDoesNotExistError> { + let Ok(mut entity) = world.get_entity_mut(entity.raw_entity()) else { + return Err(AssetEntityDoesNotExistError(entity)); + }; + + entity.insert(AssetData(asset)); + Ok(()) } -impl<'a, A: Asset> Iterator for AssetsMutIterator<'a, A> { - type Item = (AssetId, &'a mut A); - - fn next(&mut self) -> Option { - for (i, entry) in &mut self.dense_storage { - match entry { - Entry::None => { - continue; - } - Entry::Some { value, generation } => { - let id = AssetId::Index { - index: AssetIndex { - generation: *generation, - index: i as u32, - }, - marker: PhantomData, - }; - self.queued_events.push(AssetEvent::Modified { id }); - if let Some(value) = value { - return Some((id, value)); - } - } - } - } - 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)) - } else { - None +/// Writes [`AssetEvent::Added`] for this asset. +fn write_added_asset_event( + mut world: DeferredWorld, + HookContext { entity, .. }: HookContext, +) { + let entity = AssetEntity::new_unchecked(entity); + world.write_message(AssetEvent::::Added { + id: AssetId::Entity { + entity, + marker: PhantomData, + }, + }); + // TODO: Replace this with `change_tick` once it's supported. See #22788. + let tick = world.read_change_tick(); + if let Some(mut changes) = world.get_resource_mut::>() { + changes.insert(entity, tick); + } + if let Some(uuid_map) = world.get_resource::() + && let Some(uuids) = uuid_map.entity_to_uuids(entity, TypeId::of::()) + { + for uuid in uuids { + let id = AssetId::::Uuid { uuid }; + world.write_message(AssetEvent::::Added { id }); } } } -/// An error returned when an [`AssetIndex`] has an invalid generation. -#[derive(Error, Debug, PartialEq, Eq)] -pub enum InvalidGenerationError { - /// The generation of this [`AssetIndex`] does not match the current generation. - /// That means its index entry has been replaced. - #[error("AssetIndex {index:?} has an invalid generation. The current generation is: '{current_generation}'.")] - Occupied { - /// The index being looked up. - index: AssetIndex, - /// The generation currently at that index. - current_generation: u32, - }, - /// This index entry has been removed. - #[error("AssetIndex {index:?} has been removed")] - Removed { index: AssetIndex }, -} - -#[cfg(test)] -mod test { - use crate::tests::create_app; - use crate::{Asset, AssetApp, AssetEvent, AssetIndex, Assets}; - use bevy_ecs::prelude::Messages; - use bevy_reflect::TypePath; - - #[test] - fn asset_index_round_trip() { - let asset_index = AssetIndex { - generation: 42, - index: 1337, - }; - let roundtripped = AssetIndex::from_bits(asset_index.to_bits()); - assert_eq!(asset_index, roundtripped); +/// Writes [`AssetEvent::Removed`] for this asset. +fn write_removed_asset_event( + mut world: DeferredWorld, + HookContext { entity, .. }: HookContext, +) { + let entity = AssetEntity::new_unchecked(entity); + world.write_message(AssetEvent::::Removed { + id: AssetId::Entity { + entity, + marker: PhantomData, + }, + }); + if let Some(mut changes) = world.get_resource_mut::>() { + changes.remove(&entity); + } + if let Some(uuid_map) = world.get_resource::() + && let Some(uuids) = uuid_map.entity_to_uuids(entity, TypeId::of::()) + { + for uuid in uuids { + let id = AssetId::::Uuid { uuid }; + world.write_message(AssetEvent::::Removed { id }); + } } +} - #[test] - fn assets_mut_change_detection() { - #[derive(Asset, TypePath, Default)] - struct TestAsset { - value: u32, +/// Writes [`AssetEvent::Modified`] messages for any assets that have changed. +// TODO: We should remove this and leave it up to users to listen for these events. +pub(crate) fn write_modified_asset_events( + assets: Query<(Entity, Ref>), Changed>>, + ticks: SystemChangeTick, + mut messages: MessageWriter>, + mut changes: Option>>, + asset_uuid_map: Option>, +) { + for (entity, data_ref) in assets.iter() { + let entity = AssetEntity::new_unchecked(entity); + // Always count added assets for `AssetChanged`. + if let Some(changes) = changes.as_mut() { + changes.insert(entity, ticks.this_run()); } + if data_ref.last_changed() == data_ref.added() { + // The change corresponds to new asset data, which would have been handled by + // `AssetData`s hooks. + continue; + } + messages.write(AssetEvent::Modified { + id: AssetId::Entity { + entity, + marker: PhantomData, + }, + }); + } - let mut app = create_app().0; - app.init_asset::(); - - let mut assets = app.world_mut().resource_mut::>(); - let my_asset_handle = assets.add(TestAsset::default()); - let my_asset_id = my_asset_handle.id(); + let Some(uuid_map) = asset_uuid_map else { + return; + }; - // check a few times just in case there are some unexpected leftover events from previous runs - for _ in 0..3 { - // check that modifying the asset value triggers an event - { - let mut assets = app.world_mut().resource_mut::>(); - let mut asset = assets.get_mut(my_asset_id).unwrap(); - asset.value += 1; - } + for (_, inner) in uuid_map.read().iter() { + for (&uuid, handle) in inner.uuid_to_handle.iter() { + let Ok((_, data_ref)) = assets.get(handle.entity().raw_entity()) else { + continue; + }; - app.update(); - - let modified_count = app - .world_mut() - .resource_mut::>>() - .drain() - .filter(|event| event.is_modified(my_asset_id)) - .count(); - - assert_eq!( - modified_count, 1, - "Asset value was changed but AssetEvent::Modified was not triggered", - ); - - // check that reading the asset value doesn't trigger an event - { - let mut assets = app.world_mut().resource_mut::>(); - let asset = assets.get_mut(my_asset_id).unwrap(); - let _temp = asset.value; + if data_ref.last_changed() == data_ref.added() { + // The change corresponds to new asset data, which would have been handled by + // `AssetData`s hooks. + continue; } + messages.write(AssetEvent::Modified { + id: AssetId::Uuid { uuid }, + }); + } + } +} - app.update(); +/// An exclusive system that despawns assets whose handles have been dropped and so are "unused". +pub(crate) fn despawn_unused_assets(world: &mut World) { + // Note: we use an exclusive system here so that despawning an asset can trigger drops of other + // handles, which themselves get despawned in the same system (until there are no more dropped + // handles). - let modified_count = app - .world_mut() - .resource_mut::>>() - .drain() - .filter(|event| event.is_modified(my_asset_id)) - .count(); + let drop_receiver = world + .resource::() + .drop_receiver + .clone(); + for (entity, type_id) in drop_receiver.try_iter() { + AssetEventUnusedWriters::write_message(world, entity, type_id) + .expect("Asset type has been registered"); - assert_eq!( - modified_count, 0, - "Asset value was not changed but AssetEvent::Modified was triggered", - ); - } + world.despawn(entity.raw_entity()); } } + +/// An error for asset actions when the entity does not exist for that asset. +#[derive(Error, Debug)] +#[error("The requested entity {0:?} does not exist")] +pub struct AssetEntityDoesNotExistError(pub AssetEntity); diff --git a/crates/bevy_asset/src/direct_access_ext.rs b/crates/bevy_asset/src/direct_access_ext.rs index 73dc615b5e6ab..80ae17115ef21 100644 --- a/crates/bevy_asset/src/direct_access_ext.rs +++ b/crates/bevy_asset/src/direct_access_ext.rs @@ -1,22 +1,81 @@ //! Add methods on `World` to simplify loading assets when all //! you have is a `World`. -use bevy_ecs::world::World; +use core::{any::TypeId, marker::PhantomData}; -use crate::{meta::Settings, Asset, AssetPath, AssetServer, Assets, Handle, LoadBuilder}; +use bevy_ecs::{ + system::{Commands, Res, SystemParam}, + world::{error::EntityMutableFetchError, DeferredWorld, World}, +}; +use thiserror::Error; +use tracing::warn; +use uuid::Uuid; + +use crate::{ + insert_asset, meta::Settings, setup_asset, Asset, AssetData, AssetEntity, + AssetEntityDoesNotExistError, AssetEvent, AssetHandleProvider, AssetId, AssetMut, AssetPath, + AssetSelfHandle, AssetServer, AssetUuidMap, EntityHandle, Handle, LoadBuilder, + ResolveUuidError, UntypedEntityHandle, UntypedHandle, +}; /// An extension trait for methods for working with assets directly from a [`World`]. pub trait DirectAssetAccessExt { - /// Insert an asset similarly to [`Assets::add`]. - fn add_asset(&mut self, asset: impl Into) -> Handle; + /// Spawn a new asset and return the handle. + #[must_use] + fn spawn_asset(&mut self, asset: A) -> Handle; + + /// Spawns a new asset referenced by `uuid`. + /// + /// Calling this multiples times with the same `uuid` will replace the asset each time - so only + /// the final asset will remain. Calling this does not free the asset if the handle is dropped: + /// UUID assets are kept alive unless they are manually despawned or set to another handle with + /// [`AssetUuidMap::set_uuid`]. + fn spawn_uuid_asset(&mut self, uuid: Uuid, asset: A) -> Handle; + + /// Attempts to insert the asset into the entity referenced by `id`. + fn insert_asset( + &mut self, + id: impl Into>, + asset: A, + ) -> Result<(), InsertAssetError>; + + /// Attempts to remove the asset from the entity referenced by `id`. + /// + /// This does not despawn the asset - this asset can still be inserted into. + fn remove_asset(&mut self, id: AssetId) -> Result; + + /// Reserves a handle for an asset that can later be inserted. + #[must_use] + fn reserve_asset_handle(&mut self) -> Handle; + /// Gets the asset data associated with the given `id`. + fn get_asset(&self, id: AssetId) -> Option<&A>; + + /// Gets the asset data (mutably) associated with the given `id`. + fn get_asset_mut(&mut self, id: AssetId) -> Option>; + + /// Gets the strong handle associated with an asset, ensuring its type matches `A`. + fn get_asset_strong_handle(&self, entity: AssetEntity) -> Option>; + + /// Gets the strong handle associated with an asset. + fn get_asset_strong_handle_untyped(&self, entity: AssetEntity) -> Option; +} + +pub trait AssetServerAccessExt { /// Load an asset similarly to [`AssetServer::load`]. + #[must_use] fn load_asset<'a, A: Asset>(&self, path: impl Into>) -> Handle; /// Creates a new [`LoadBuilder`] similar to [`AssetServer::load_builder`]. + /// + /// # Panics + /// If `self` doesn't have an [`AssetServer`] resource initialized yet. fn load_builder(&self) -> LoadBuilder<'_>; /// Load an asset with settings, similarly to [`AssetServer::load_with_settings`]. + /// + /// # Panics + /// If `self` doesn't have an [`AssetServer`] resource initialized yet. #[deprecated(note = "Use `world.load_builder().with_settings(settings).load(path)`")] fn load_asset_with_settings<'a, A: Asset, S: Settings>( &self, @@ -26,34 +85,95 @@ pub trait DirectAssetAccessExt { } impl DirectAssetAccessExt for World { - /// Insert an asset similarly to [`Assets::add`]. - /// - /// # Panics - /// If `self` doesn't have an [`AssetServer`] resource initialized yet. - fn add_asset<'a, A: Asset>(&mut self, asset: impl Into) -> Handle { - self.resource_mut::>().add(asset) + fn spawn_asset(&mut self, asset: A) -> Handle { + let handle = self.reserve_asset_handle(); + let entity = handle.entity().unwrap(); + insert_asset(self, entity, asset).unwrap(); + handle } - /// Load an asset similarly to [`AssetServer::load`]. - /// - /// # Panics - /// If `self` doesn't have an [`AssetServer`] resource initialized yet. + fn spawn_uuid_asset(&mut self, uuid: Uuid, asset: A) -> Handle { + let handle = self.spawn_asset(asset); + // spawn_asset always returns a Strong variant, so unwrap here is safe. + let entity_handle = UntypedEntityHandle::from(EntityHandle::try_from(handle).unwrap()); + self.resource_mut::() + .set_uuid(uuid, entity_handle); + // Send an asset event that this UUID has been added. + // TODO: This isn't quite sufficient, since users can call set_uuid themselves. We should be + // sending a message in that case as well. This also doesn't work correctly if the UUID + // was already set. + self.write_message(AssetEvent::::Added { + id: AssetId::Uuid { uuid }, + }); + Handle::Uuid(uuid, PhantomData) + } + + fn insert_asset( + &mut self, + id: impl Into>, + asset: A, + ) -> Result<(), InsertAssetError> { + let entity = self.resource::().resolve_entity(id.into())?; + insert_asset(self, entity, asset)?; + Ok(()) + } + + fn remove_asset(&mut self, id: AssetId) -> Result { + let entity = self.resource::().resolve_entity(id)?; + let mut entity_mut = self.get_entity_mut(entity.raw_entity())?; + let data = entity_mut + .take::>() + .ok_or(RemoveAssetError::AssetMissingData(entity))?; + Ok(data.0) + } + + fn reserve_asset_handle(&mut self) -> Handle { + let entity = AssetEntity::new_unchecked(self.spawn_empty().id()); + let handle = self.resource::().create_handle( + entity, + TypeId::of::(), + None, + None, + ); + setup_asset(self, &handle).unwrap(); + Handle::Strong(handle) + } + + fn get_asset(&self, id: AssetId) -> Option<&A> { + let entity = self.resource::().resolve_entity(id).ok()?; + let entity = self.get_entity(entity.raw_entity()).ok()?; + let data = entity.get::>()?; + Some(&data.0) + } + + fn get_asset_mut(&mut self, id: AssetId) -> Option> { + let entity = self.resource::().resolve_entity(id).ok()?; + let entity = self.get_entity_mut(entity.raw_entity()).ok()?; + Some(AssetMut(entity.into_mut::>()?)) + } + + fn get_asset_strong_handle(&self, entity: AssetEntity) -> Option> { + let entity = self.get_entity(entity.raw_entity()).ok()?; + let handle = entity.get::()?; + handle.upgrade().ok() + } + + fn get_asset_strong_handle_untyped(&self, entity: AssetEntity) -> Option { + let entity = self.get_entity(entity.raw_entity()).ok()?; + let handle = entity.get::()?; + handle.upgrade_untyped() + } +} + +impl AssetServerAccessExt for World { fn load_asset<'a, A: Asset>(&self, path: impl Into>) -> Handle { self.resource::().load(path) } - /// Creates a new [`LoadBuilder`] similar to [`AssetServer::load_builder`]. - /// - /// # Panics - /// If `self` doesn't have an [`AssetServer`] resource initialized yet. fn load_builder(&self) -> LoadBuilder<'_> { self.resource::().load_builder() } - /// Load an asset with settings, similarly to [`AssetServer::load_with_settings`]. - /// - /// # Panics - /// If `self` doesn't have an [`AssetServer`] resource initialized yet. fn load_asset_with_settings<'a, A: Asset, S: Settings>( &self, path: impl Into>, @@ -65,3 +185,281 @@ impl DirectAssetAccessExt for World { .load(path.into()) } } + +pub trait WorldAssetCommandsExt { + /// Creates a new [`WorldAssetCommands`] instance that writes to the world's command queue. + /// + /// Use [`World::flush`] to apply all queued commands. + fn asset_commands(&mut self) -> WorldAssetCommands<'_, '_>; +} + +impl WorldAssetCommandsExt for World { + fn asset_commands(&mut self) -> WorldAssetCommands<'_, '_> { + WorldAssetCommands { + provider: self.resource::().clone(), + commands: self.commands(), + } + } +} + +impl WorldAssetCommandsExt for DeferredWorld<'_> { + fn asset_commands(&mut self) -> WorldAssetCommands<'_, '_> { + WorldAssetCommands { + provider: self.resource::().clone(), + commands: self.commands(), + } + } +} + +/// A [`SystemParam`] with commands for manipulating assets. +/// +/// Similar to [`Commands`], these actions are applied at the next sync point. +#[derive(SystemParam)] +pub struct AssetCommands<'w, 's> { + /// Commands to use for creating new asset. + commands: Commands<'w, 's>, + /// The provider for the actual handles. + provider: Res<'w, AssetHandleProvider>, +} + +impl<'w, 's> AssetCommands<'w, 's> { + /// Spawn a new asset and return the handle. + #[must_use] + pub fn spawn_asset(&mut self, asset: A) -> Handle { + InternalAssetCommands { + commands: &mut self.commands, + provider: &self.provider, + } + .spawn_asset(asset) + } + + /// Spawns a new asset referenced by `uuid`. + /// + /// Calling this multiples times with the same `uuid` will replace the asset each time - so only + /// the final asset will remain. Calling this does not free the asset if the handle is dropped: + /// UUID assets are kept alive unless they are manually despawned or set to another handle with + /// [`AssetUuidMap::set_uuid`]. + pub fn spawn_uuid_asset(&mut self, uuid: Uuid, asset: A) -> Handle { + InternalAssetCommands { + commands: &mut self.commands, + provider: &self.provider, + } + .spawn_uuid_asset(uuid, asset) + } + + /// Attempts to insert the asset into the entity referenced by `id`. + pub fn insert_asset(&mut self, id: impl Into>, asset: A) { + InternalAssetCommands { + commands: &mut self.commands, + provider: &self.provider, + } + .insert_asset(id.into(), asset); + } + + /// Removes the asset referenced by `id`. + pub fn remove_asset(&mut self, id: AssetId) { + InternalAssetCommands { + commands: &mut self.commands, + provider: &self.provider, + } + .remove_asset(id); + } + + /// Reserves a handle for an asset of type `A`. + #[must_use] + pub fn reserve_handle(&mut self) -> Handle { + InternalAssetCommands { + commands: &mut self.commands, + provider: &self.provider, + } + .reserve_handle() + } + + /// Returns the internal [`Commands`] used to update assets. + /// + /// This is an "escape hatch" to allow adding commands to the same command buffer as that used + /// for assets. This allows users to ensure enqueued commands run after any assets are + /// spawned/inserted/removed. + pub fn commands(&mut self) -> &mut Commands<'w, 's> { + &mut self.commands + } +} + +/// A version of [`AssetCommands`] which is not a [`SystemParam`], allowing it to be used with +/// [`DeferredWorld`] access. +pub struct WorldAssetCommands<'w, 's> { + /// Commands to use for creating new asset. + commands: Commands<'w, 's>, + /// The provider for the actual handles. + provider: AssetHandleProvider, +} + +impl<'w, 's> WorldAssetCommands<'w, 's> { + /// Spawn a new asset and return the handle. + #[must_use] + pub fn spawn_asset(&mut self, asset: A) -> Handle { + InternalAssetCommands { + commands: &mut self.commands, + provider: &self.provider, + } + .spawn_asset(asset) + } + + /// Spawns a new asset referenced by `uuid`. + /// + /// Calling this multiples times with the same `uuid` will replace the asset each time - so only + /// the final asset will remain. Calling this does not free the asset if the handle is dropped: + /// UUID assets are kept alive unless they are manually despawned or set to another handle with + /// [`AssetUuidMap::set_uuid`]. + pub fn spawn_uuid_asset(&mut self, uuid: Uuid, asset: A) -> Handle { + InternalAssetCommands { + commands: &mut self.commands, + provider: &self.provider, + } + .spawn_uuid_asset(uuid, asset) + } + + /// Attempts to insert the asset into the entity referenced by `id`. + pub fn insert_asset(&mut self, id: impl Into>, asset: A) { + InternalAssetCommands { + commands: &mut self.commands, + provider: &self.provider, + } + .insert_asset(id.into(), asset); + } + + /// Removes the asset referenced by `id`. + pub fn remove_asset(&mut self, id: AssetId) { + InternalAssetCommands { + commands: &mut self.commands, + provider: &self.provider, + } + .remove_asset(id); + } + + /// Reserves a handle for an asset of type `A`. + #[must_use] + pub fn reserve_handle(&mut self) -> Handle { + InternalAssetCommands { + commands: &mut self.commands, + provider: &self.provider, + } + .reserve_handle() + } + + /// Returns the internal [`Commands`] used to update assets. + /// + /// This is an "escape hatch" to allow adding commands to the same command buffer as that used + /// for assets. This allows users to ensure enqueued commands run after any assets are + /// spawned/inserted/removed. + pub fn commands(&mut self) -> &mut Commands<'w, 's> { + &mut self.commands + } +} + +/// Implementation of [`AssetCommands`] and [`WorldAssetCommands`]. +struct InternalAssetCommands<'a, 'w, 's> { + /// Commands to use for creating new asset. + commands: &'a mut Commands<'w, 's>, + /// The provider for the actual handles. + provider: &'a AssetHandleProvider, +} + +impl InternalAssetCommands<'_, '_, '_> { + /// Same as [`AssetCommands::spawn_asset`]. + #[must_use] + fn spawn_asset(&mut self, asset: A) -> Handle { + let handle = self.reserve_handle(); + // reserve_handle always returns a Strong so unwrapping is safe. + let entity = handle.entity().unwrap(); + self.commands.queue(move |world: &mut World| { + // We just spawned the asset, so inserting into it should be safe. + insert_asset(world, entity, asset).unwrap(); + }); + handle + } + + /// Same as [`AssetCommands::spawn_uuid_asset`]. + fn spawn_uuid_asset(&mut self, uuid: Uuid, asset: A) -> Handle { + let handle = self.spawn_asset::(asset); + // spawn_asset always returns a Strong variant, so unwrap here is safe. + let entity_handle = UntypedEntityHandle::from(EntityHandle::try_from(handle).unwrap()); + self.commands.queue(move |world: &mut World| { + world + .resource_mut::() + .set_uuid(uuid, entity_handle); + // Send an asset event that this UUID has been added. + // TODO: This isn't quite sufficient, since users can call set_uuid themselves. We should be + // sending a message in that case as well. This also doesn't work correctly if the UUID + // was already set. + world.write_message(AssetEvent::::Added { + id: AssetId::Uuid { uuid }, + }); + }); + Handle::Uuid(uuid, PhantomData) + } + + /// Same as [`AssetCommands::insert_asset`]. + fn insert_asset(&mut self, id: AssetId, asset: A) { + self.commands.queue(move |world: &mut World| { + let entity = match world.resource::().resolve_entity(id) { + Ok(entity) => entity, + Err(err) => { + warn!("Failed to insert into handle: {err}"); + return; + } + }; + if let Err(err) = insert_asset(world, entity, asset) { + warn!("Failed to insert asset: {err}"); + } + }); + } + + /// Same as [`AssetCommands::remove_asset`]. + fn remove_asset(&mut self, id: AssetId) { + self.commands.queue(move |world: &mut World| { + let _ = world.remove_asset(id); + }); + } + + /// Same as [`AssetCommands::reserve_handle`]. + #[must_use] + fn reserve_handle(&mut self) -> Handle { + let entity = AssetEntity::new_unchecked(self.commands.spawn_empty().id()); + let handle = self + .provider + .create_handle(entity, TypeId::of::(), None, None); + { + let handle = handle.clone(); + self.commands.queue(move |world: &mut World| { + setup_asset(world, &handle).unwrap(); + }); + } + Handle::Strong(handle) + } +} + +/// An error when inserting an asset. +#[derive(Error, Debug)] +pub enum InsertAssetError { + /// The handle held a UUID which is not assigned. + #[error(transparent)] + ResolveHandle(#[from] ResolveUuidError), + /// The entity for the handle does not exist. + #[error(transparent)] + EntityDoesNotExist(#[from] AssetEntityDoesNotExistError), +} + +/// An error when inserting an asset. +#[derive(Error, Debug)] +pub enum RemoveAssetError { + /// The id held a UUID which is not assigned. + #[error(transparent)] + ResolveHandle(#[from] ResolveUuidError), + /// The entity for the id does not exist. + #[error(transparent)] + EntityFetchError(#[from] EntityMutableFetchError), + /// The entity does not contain the given asset data. + #[error("The entity {0} does not contain asset data")] + AssetMissingData(AssetEntity), +} diff --git a/crates/bevy_asset/src/event.rs b/crates/bevy_asset/src/event.rs index 645df4896f7be..954af84e9b25f 100644 --- a/crates/bevy_asset/src/event.rs +++ b/crates/bevy_asset/src/event.rs @@ -1,7 +1,9 @@ -use crate::{Asset, AssetId, AssetLoadError, AssetPath, UntypedAssetId}; -use bevy_ecs::message::Message; +use crate::{Asset, AssetEntity, AssetId, AssetLoadError, AssetPath, UntypedAssetId}; +use bevy_ecs::{message::Message, resource::Resource, world::World}; use bevy_reflect::Reflect; -use core::fmt::Debug; +use bevy_utils::TypeIdMap; +use core::{any::TypeId, fmt::Debug, marker::PhantomData}; +use thiserror::Error; /// A [`Message`] emitted when a specific [`Asset`] fails to load. /// @@ -127,3 +129,45 @@ impl PartialEq for AssetEvent { } impl Eq for AssetEvent {} + +/// Type-erased registry for writing [`AssetEvent::Unused`] messages. +#[derive(Resource, Default)] +pub(crate) struct AssetEventUnusedWriters(TypeIdMap); + +impl AssetEventUnusedWriters { + /// Registers asset of type `A` to be able to send its [`AssetEvent::Unused`]. + pub(crate) fn register(&mut self) { + fn write_unused_message(world: &mut World, entity: AssetEntity) { + world.write_message(AssetEvent::::Unused { + id: AssetId::Entity { + entity, + marker: PhantomData, + }, + }); + } + + self.0.insert(TypeId::of::(), write_unused_message::); + } + + /// Writes an [`AssetEvent::Unused`] message for an asset of type `type_id`. + /// + /// This is not a method so that we don't need to hokey-pokey this type. + pub(crate) fn write_message( + world: &mut World, + entity: AssetEntity, + type_id: TypeId, + ) -> Result<(), MissingAssetTypeError> { + let func = world + .resource::() + .0 + .get(&type_id) + .ok_or(MissingAssetTypeError(type_id))?; + func(world, entity); + Ok(()) + } +} + +/// The provided asset type could not be found. +#[derive(Error, Debug)] +#[error("Failed to find asset type {0:?}")] +pub(crate) struct MissingAssetTypeError(pub(crate) TypeId); diff --git a/crates/bevy_asset/src/handle.rs b/crates/bevy_asset/src/handle.rs index 3f11d7818b59e..3945eb79d5659 100644 --- a/crates/bevy_asset/src/handle.rs +++ b/crates/bevy_asset/src/handle.rs @@ -1,9 +1,13 @@ use crate::{ - meta::MetaTransform, Asset, AssetId, AssetIndex, AssetIndexAllocator, AssetPath, AssetServer, - ErasedAssetIndex, ReflectHandle, UntypedAssetId, + meta::MetaTransform, Asset, AssetEntity, AssetId, AssetPath, AssetServer, ReflectHandle, + UntypedAssetId, +}; +use alloc::sync::{Arc, Weak}; +use bevy_ecs::{ + component::Component, + resource::Resource, + template::{FromTemplate, SpecializeFromTemplate, Template, TemplateContext}, }; -use alloc::sync::Arc; -use bevy_ecs::template::{FromTemplate, SpecializeFromTemplate, Template, TemplateContext}; use bevy_platform::{collections::Equivalent, sync::Mutex}; use bevy_reflect::{enums::Enum, FromReflect, PartialReflect, Reflect, ReflectRef, TypePath}; use core::{ @@ -11,119 +15,280 @@ use core::{ hash::{Hash, Hasher}, marker::PhantomData, }; -use crossbeam_channel::{Receiver, Sender}; +use crossbeam_channel::{bounded, never, Receiver, Sender}; use disqualified::ShortName; use thiserror::Error; use uuid::Uuid; -/// Provides [`Handle`] and [`UntypedHandle`] _for a specific asset type_. -/// This should _only_ be used for one specific asset type. -#[derive(Clone)] -pub struct AssetHandleProvider { - pub(crate) allocator: Arc, - pub(crate) drop_sender: Sender, - pub(crate) drop_receiver: Receiver, - pub(crate) type_id: TypeId, +/// Stores the handle of this asset (weakly). +/// +/// This allows users to access an asset through a [`Query`](bevy_ecs::system::Query) and get its +/// handle. +/// +/// This stores a "weak" handle, preventing the asset from keeping itself from being dropped. +/// Attempting to access the "strong" handle may fail if this asset is already enqueued for despawn. +#[derive(Component)] +pub struct AssetSelfHandle(pub(crate) Weak); + +impl AssetSelfHandle { + /// Attempts to get a [`Handle`] for this asset, assuming it is the given type. + pub fn upgrade(&self) -> Result, HandleUpgradeError> { + let handle = self + .upgrade_untyped() + .ok_or(HandleUpgradeError::ExpiredHandle)?; + Ok(handle.try_typed()?) + } + + /// Attempts to get an [`UntypedHandle`] to this asset. + /// + /// This may return [`None`] if the asset handle has expired, meaning that the asset is already + /// enqueued for despawn. + pub fn upgrade_untyped(&self) -> Option { + self.0.upgrade().map(UntypedHandle::Strong) + } } -#[derive(Debug)] -pub(crate) struct DropEvent { - pub(crate) index: ErasedAssetIndex, - pub(crate) asset_server_managed: bool, +/// Error for upgrading a "weak" (and untyped) handle into a strong, typed handle. +#[derive(Error, Debug, Clone)] +pub enum HandleUpgradeError { + #[error("The underlying handle has been dropped, so the handle can't be upgraded")] + ExpiredHandle, + #[error(transparent)] + WrongType(#[from] UntypedAssetConversionError), +} + +/// Provides [`Handle`] and [`UntypedHandle`]s for an entity. +#[derive(Resource, Clone)] +pub(crate) struct AssetHandleProvider { + // TODO: We only need the TypeId for sending AssetEvent::Unused. Remove the TypeId once we're + // using events. + pub(crate) drop_sender: Sender<(AssetEntity, TypeId)>, + pub(crate) drop_receiver: Receiver<(AssetEntity, TypeId)>, } impl AssetHandleProvider { - pub(crate) fn new(type_id: TypeId, allocator: Arc) -> Self { + /// Creates a fake provider, for use in cases where we don't need real handles. + pub(crate) fn fake() -> Self { + // Create a totally fake channel. The sender channel is already closed, and the receiver is + // just the `never` channel. + let (sender, _) = bounded(0); + let receiver = never(); + Self { + drop_sender: sender, + drop_receiver: receiver, + } + } + + /// Creates a new instance of a handle provider. + pub(crate) fn new() -> Self { let (drop_sender, drop_receiver) = crossbeam_channel::unbounded(); Self { - type_id, - allocator, drop_sender, drop_receiver, } } - /// Reserves a new strong [`UntypedHandle`] (with a new [`UntypedAssetId`]). The stored [`Asset`] [`TypeId`] in the - /// [`UntypedHandle`] will match the [`Asset`] [`TypeId`] assigned to this [`AssetHandleProvider`]. - pub fn reserve_handle(&self) -> UntypedHandle { - let index = self.allocator.reserve(); - UntypedHandle::Strong(self.get_handle(index, false, None, None)) - } - - pub(crate) fn get_handle( + /// Creates a handle with all its asset's details provided. + pub(crate) fn create_handle( &self, - index: AssetIndex, - asset_server_managed: bool, + entity: AssetEntity, + type_id: TypeId, path: Option>, meta_transform: Option, ) -> Arc { Arc::new(StrongHandle { - index, - type_id: self.type_id, - drop_sender: self.drop_sender.clone(), + entity, + type_id, meta_transform, path, - asset_server_managed, + drop_sender: self.drop_sender.clone(), }) } - - pub(crate) fn reserve_handle_internal( - &self, - asset_server_managed: bool, - path: Option>, - meta_transform: Option, - ) -> Arc { - let index = self.allocator.reserve(); - self.get_handle(index, asset_server_managed, path, meta_transform) - } } /// The internal "strong" [`Asset`] handle storage for [`Handle::Strong`] and [`UntypedHandle::Strong`]. When this is dropped, /// the [`Asset`] will be freed. It also stores some asset metadata for easy access from handles. #[derive(TypePath)] pub struct StrongHandle { - pub(crate) index: AssetIndex, + pub(crate) entity: AssetEntity, pub(crate) type_id: TypeId, - pub(crate) asset_server_managed: bool, pub(crate) path: Option>, /// Modifies asset meta. This is stored on the handle because it is: /// 1. configuration tied to the lifetime of a specific asset load /// 2. configuration that must be repeatable when the asset is hot-reloaded pub(crate) meta_transform: Option, - pub(crate) drop_sender: Sender, + pub(crate) drop_sender: Sender<(AssetEntity, TypeId)>, } impl Drop for StrongHandle { fn drop(&mut self) { - let _ = self.drop_sender.send(DropEvent { - index: ErasedAssetIndex::new(self.index, self.type_id), - asset_server_managed: self.asset_server_managed, - }); + let _ = self.drop_sender.send((self.entity, self.type_id)); } } impl core::fmt::Debug for StrongHandle { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("StrongHandle") - .field("index", &self.index) + .field("entity", &self.entity) .field("type_id", &self.type_id) - .field("asset_server_managed", &self.asset_server_managed) .field("path", &self.path) .field("drop_sender", &self.drop_sender) .finish() } } +/// A handle to an asset entity whose type information only exists at runtime. +/// +/// Unlike [`UntypedHandle`], this handle is guaranteed to be pointed at an entity, and not at a +/// UUID. +#[derive(Clone, Debug)] +pub struct UntypedEntityHandle(pub(crate) Arc); + +impl UntypedEntityHandle { + /// Returns the asset entity this handle references. + #[inline] + pub fn entity(&self) -> AssetEntity { + self.0.entity + } + + /// Returns the [`UntypedAssetId`] for this handle. + #[inline] + pub fn id(&self) -> UntypedAssetId { + UntypedAssetId::Entity { + type_id: self.0.type_id, + entity: self.0.entity, + } + } + + /// Returns the type ID of the asset being referenced. + pub fn type_id(&self) -> TypeId { + self.0.type_id + } + + /// Returns the path if the asset has a path. + #[inline] + pub fn path(&self) -> Option<&AssetPath<'static>> { + self.0.path.as_ref() + } + + /// Converts to a typed Handle. This _will not check if the target Handle type matches_. + #[inline] + pub fn typed_unchecked(self) -> EntityHandle { + EntityHandle(self.0, PhantomData) + } + + /// Converts to a typed Handle. This will check the type when compiled with debug asserts, but it + /// _will not check if the target Handle type matches in release builds_. Use this as an optimization + /// when you want some degree of validation at dev-time, but you are also very certain that the type + /// actually matches. + #[inline] + pub fn typed_debug_checked(self) -> EntityHandle { + debug_assert_eq!( + self.0.type_id, + TypeId::of::(), + "The target EntityHandle's TypeId does not match the TypeId of this UntypedEntityHandle" + ); + self.typed_unchecked() + } + + /// Converts to a typed Handle. This will panic if the internal [`TypeId`] does not match the given asset type `A` + #[inline] + pub fn typed(self) -> EntityHandle { + let Ok(handle) = self.try_typed() else { + panic!( + "The target EntityHandle<{}>'s TypeId does not match the TypeId of this UntypedEntityHandle", + core::any::type_name::() + ) + }; + + handle + } + + /// Converts to a typed Handle. This will panic if the internal [`TypeId`] does not match the given asset type `A` + #[inline] + pub fn try_typed(self) -> Result, UntypedAssetConversionError> { + EntityHandle::try_from(self) + } +} + +/// A handle to an asset entity for the `A` [`Asset`]. +/// +/// Unlike [`Handle`], this handle is guaranteed to be pointed at an entity, and not at a UUID. +pub struct EntityHandle(Arc, PhantomData A>); + +impl EntityHandle { + /// Returns the asset entity this handle references. + #[inline] + pub fn entity(&self) -> AssetEntity { + self.0.entity + } + + /// Returns the [`AssetId`] for this handle. + #[inline] + pub fn id(&self) -> AssetId { + AssetId::Entity { + entity: self.0.entity, + marker: PhantomData, + } + } + + /// Returns the path if the asset has a path. + #[inline] + pub fn path(&self) -> Option<&AssetPath<'static>> { + self.0.path.as_ref() + } + + /// Converts this [`EntityHandle`] to an "untyped" / "generic-less" [`UntypedEntityHandle`], + /// which stores the [`Asset`] type information _inside_ [`UntypedEntityHandle`]. + #[inline] + pub fn untyped(self) -> UntypedEntityHandle { + self.into() + } +} + +impl TryFrom for EntityHandle { + type Error = UntypedAssetConversionError; + + fn try_from(value: UntypedEntityHandle) -> Result { + let found = value.0.type_id; + let expected = TypeId::of::(); + if found == expected { + return Err(UntypedAssetConversionError::TypeIdMismatch { expected, found }); + } + + Ok(value.typed_unchecked()) + } +} + +impl From> for UntypedEntityHandle { + fn from(value: EntityHandle) -> Self { + UntypedEntityHandle(value.0) + } +} + +impl core::fmt::Debug for EntityHandle { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple("EntityHandle").field(&self.0).finish() + } +} + +impl Clone for EntityHandle { + fn clone(&self) -> Self { + Self(self.0.clone(), PhantomData) + } +} + /// A handle to a specific [`Asset`] of type `A`. Handles act as abstract "references" to -/// assets, whose data are stored in the [`Assets`](crate::prelude::Assets) resource, +/// assets, whose data are stored on entities in the [`AssetData`](crate::AssetData) component, /// avoiding the need to store multiple copies of the same data. /// /// If a [`Handle`] is [`Handle::Strong`], the [`Asset`] will be kept -/// alive until the [`Handle`] is dropped. If a [`Handle`] is [`Handle::Uuid`], it does not necessarily reference a live [`Asset`], -/// nor will it keep assets alive. +/// alive until the [`Handle`] is dropped. If a [`Handle`] is [`Handle::Uuid`], it does not +/// necessarily reference a live [`Asset`]. /// /// Modifying a *handle* will change which existing asset is referenced, but modifying the *asset* -/// (by mutating the [`Assets`](crate::prelude::Assets) resource) will change the asset for all handles referencing it. +/// (by mutating the data in [`AssetData`](crate::AssetData)) will change the asset for all handles +/// referencing it. /// /// [`Handle`] can be cloned. If a [`Handle::Strong`] is cloned, the referenced [`Asset`] will not be freed until _all_ instances /// of the [`Handle`] are dropped. @@ -185,15 +350,29 @@ impl Clone for Handle { } impl Handle { - /// Returns the [`AssetId`] of this [`Asset`]. + /// Returns the entity if this is a strong handle. + /// + /// While a UUID could refer to an entity as well, the handle must first be resolved with + /// [`crate::AssetUuidMap::resolve_handle`]. In general, using + /// [`crate::AssetUuidMap::resolve_handle`] should be preferred to allow for maximum + /// flexibility. + #[inline] + pub fn entity(&self) -> Option { + match self { + Self::Strong(handle) => Some(handle.entity), + Self::Uuid(..) => None, + } + } + + /// Returns the [`AssetId`] for this handle. #[inline] pub fn id(&self) -> AssetId { match self { - Handle::Strong(handle) => AssetId::Index { - index: handle.index, + Self::Strong(handle) => AssetId::Entity { + entity: handle.entity, marker: PhantomData, }, - Handle::Uuid(uuid, ..) => AssetId::Uuid { uuid: *uuid }, + Self::Uuid(uuid, _) => AssetId::Uuid { uuid: *uuid }, } } @@ -201,8 +380,17 @@ impl Handle { #[inline] pub fn path(&self) -> Option<&AssetPath<'static>> { match self { - Handle::Strong(handle) => handle.path.as_ref(), - Handle::Uuid(..) => None, + Self::Strong(handle) => handle.path.as_ref(), + Self::Uuid(..) => None, + } + } + + /// Returns the UUID if this is a UUID handle. + #[inline] + pub fn uuid(&self) -> Option { + match self { + Self::Uuid(uuid, _) => Some(*uuid), + Self::Strong(_) => None, } } @@ -385,8 +573,8 @@ impl core::fmt::Debug for Handle { Handle::Strong(handle) => { write!( f, - "StrongHandle<{name}>{{ index: {:?}, type_id: {:?}, path: {:?} }}", - handle.index, handle.type_id, handle.path + "StrongHandle<{name}>{{ entity: {:?}, type_id: {:?}, path: {:?} }}", + handle.entity, handle.type_id, handle.path ) } Handle::Uuid(uuid, ..) => write!(f, "UuidHandle<{name}>({uuid:?})"), @@ -429,34 +617,6 @@ impl PartialEq for Handle { impl Eq for Handle {} -impl From<&Handle> for AssetId { - #[inline] - fn from(value: &Handle) -> Self { - value.id() - } -} - -impl From<&Handle> for UntypedAssetId { - #[inline] - fn from(value: &Handle) -> Self { - value.id().into() - } -} - -impl From<&mut Handle> for AssetId { - #[inline] - fn from(value: &mut Handle) -> Self { - value.id() - } -} - -impl From<&mut Handle> for UntypedAssetId { - #[inline] - fn from(value: &mut Handle) -> Self { - value.id().into() - } -} - impl From for Handle { #[inline] fn from(uuid: Uuid) -> Self { @@ -473,7 +633,7 @@ impl From for Handle { pub enum UntypedHandle { /// A strong handle, which will keep the referenced [`Asset`] alive until all strong handles are dropped. Strong(Arc), - /// A UUID handle, which does not keep the referenced [`Asset`] alive. + /// A UUID handle. Dropping this handle will not result in the asset being dropped. Uuid { /// An identifier that records the underlying asset type. type_id: TypeId, @@ -491,15 +651,27 @@ impl UntypedHandle { } } - /// Returns the [`UntypedAssetId`] for the referenced asset. + /// Returns the entity if this is a strong handle. + /// + /// While a UUID could refer to an entity as well, the handle must first be resolved with + /// [`crate::AssetUuidMap::resolve_untyped_handle`]. + #[inline] + pub fn entity(&self) -> Option { + match self { + Self::Strong(handle) => Some(handle.entity), + Self::Uuid { .. } => None, + } + } + + /// Returns the [`UntypedAssetId`] for this handle. #[inline] pub fn id(&self) -> UntypedAssetId { match self { - UntypedHandle::Strong(handle) => UntypedAssetId::Index { + Self::Strong(handle) => UntypedAssetId::Entity { + entity: handle.entity, type_id: handle.type_id, - index: handle.index, }, - UntypedHandle::Uuid { type_id, uuid } => UntypedAssetId::Uuid { + Self::Uuid { uuid, type_id } => UntypedAssetId::Uuid { uuid: *uuid, type_id: *type_id, }, @@ -515,6 +687,15 @@ impl UntypedHandle { } } + /// Returns the UUID if this is a UUID handle. + #[inline] + pub fn uuid(&self) -> Option { + match self { + Self::Uuid { uuid, .. } => Some(*uuid), + Self::Strong(_) => None, + } + } + /// Returns the [`TypeId`] of the referenced [`Asset`]. #[inline] pub fn type_id(&self) -> TypeId { @@ -580,7 +761,7 @@ impl UntypedHandle { impl PartialEq for UntypedHandle { #[inline] fn eq(&self, other: &Self) -> bool { - self.id() == other.id() && self.type_id() == other.type_id() + self.id() == other.id() } } @@ -599,8 +780,8 @@ impl core::fmt::Debug for UntypedHandle { UntypedHandle::Strong(handle) => { write!( f, - "StrongHandle{{ type_id: {:?}, id: {:?}, path: {:?} }}", - handle.type_id, handle.index, handle.path + "StrongHandle{{ type_id: {:?}, entity: {:?}, path: {:?} }}", + handle.type_id, handle.entity, handle.path ) } UntypedHandle::Uuid { type_id, uuid } => { @@ -612,18 +793,7 @@ impl core::fmt::Debug for UntypedHandle { impl PartialOrd for UntypedHandle { fn partial_cmp(&self, other: &Self) -> Option { - if self.type_id() == other.type_id() { - self.id().partial_cmp(&other.id()) - } else { - None - } - } -} - -impl From<&UntypedHandle> for UntypedAssetId { - #[inline] - fn from(value: &UntypedHandle) -> Self { - value.id() + self.id().partial_cmp(&other.id()) } } @@ -632,7 +802,7 @@ impl From<&UntypedHandle> for UntypedAssetId { impl PartialEq for Handle { #[inline] fn eq(&self, other: &UntypedHandle) -> bool { - TypeId::of::() == other.type_id() && self.id() == other.id() + self.id() == other.id() } } @@ -646,11 +816,7 @@ impl PartialEq> for UntypedHandle { impl PartialOrd for Handle { #[inline] fn partial_cmp(&self, other: &UntypedHandle) -> Option { - if TypeId::of::() != other.type_id() { - None - } else { - self.id().partial_cmp(&other.id()) - } + self.id().partial_cmp(&other.id()) } } @@ -691,6 +857,64 @@ impl TryFrom for Handle { } } +impl TryFrom> for EntityHandle { + type Error = HandleToEntityHandleError; + + fn try_from(value: Handle) -> Result { + match value { + Handle::Uuid(uuid, _) => Err(HandleToEntityHandleError::UuidHandle(uuid)), + Handle::Strong(inner) => Ok(EntityHandle(inner, PhantomData)), + } + } +} + +impl TryFrom for UntypedEntityHandle { + type Error = HandleToEntityHandleError; + + fn try_from(value: UntypedHandle) -> Result { + match value { + UntypedHandle::Uuid { uuid, .. } => Err(HandleToEntityHandleError::UuidHandle(uuid)), + UntypedHandle::Strong(inner) => Ok(UntypedEntityHandle(inner)), + } + } +} + +impl From<&Handle> for AssetId { + fn from(value: &Handle) -> Self { + value.id() + } +} + +impl From<&Handle> for UntypedAssetId { + fn from(value: &Handle) -> Self { + value.id().into() + } +} + +impl From<&mut Handle> for AssetId { + fn from(value: &mut Handle) -> Self { + value.id() + } +} + +impl From<&mut Handle> for UntypedAssetId { + fn from(value: &mut Handle) -> Self { + value.id().into() + } +} + +impl From<&UntypedHandle> for UntypedAssetId { + fn from(value: &UntypedHandle) -> Self { + value.id() + } +} + +impl From<&mut UntypedHandle> for UntypedAssetId { + fn from(value: &mut UntypedHandle) -> Self { + value.id() + } +} + /// Creates a [`Handle`] from a string literal containing a UUID. /// /// # Examples @@ -731,6 +955,15 @@ pub enum UntypedAssetConversionError { }, } +/// An error for when trying to convert a [`Handle`]/[`UntypedHandle`] into an +/// [`EntityHandle`]/[`UntypedEntityHandle`]. +#[derive(Error, Debug)] +pub enum HandleToEntityHandleError { + /// The handle is not an entity handle. + #[error("The handle being converted is a UUID handle, not an entity handle")] + UuidHandle(Uuid), +} + #[cfg(test)] mod tests { use alloc::boxed::Box; @@ -739,7 +972,10 @@ mod tests { use core::hash::BuildHasher; use uuid::Uuid; - use crate::{tests::create_app, AssetApp, Assets, VisitAssetDependencies}; + use crate::{ + AssetApp, VisitAssetDependencies, + {tests::create_app, DirectAssetAccessExt}, + }; use super::*; @@ -839,7 +1075,7 @@ mod tests { let handle: Handle = uuid.into(); assert!(handle.is_uuid()); - assert_eq!(handle.id(), AssetId::Uuid { uuid }); + assert_eq!(handle.id(), AssetId::::Uuid { uuid }); } /// `PartialReflect::reflect_clone`/`PartialReflect::to_dynamic` should increase the strong count of a strong handle @@ -851,14 +1087,13 @@ mod tests { } impl Asset for MyAsset {} impl VisitAssetDependencies for MyAsset { - fn visit_dependencies(&self, _visit: &mut impl FnMut(UntypedAssetId)) {} + fn visit_dependencies(&self, _visit: &mut impl FnMut(AssetEntity)) {} } let mut app = create_app().0; app.init_asset::(); - let mut assets = app.world_mut().resource_mut::>(); - let handle: Handle = assets.add(MyAsset { value: 1 }); + let handle: Handle = app.world_mut().spawn_asset(MyAsset { value: 1 }); match &handle { Handle::Strong(strong) => { assert_eq!( @@ -907,8 +1142,7 @@ mod tests { let mut app = create_app().0; app.init_asset::().init_asset::(); - let mut assets = app.world_mut().resource_mut::>(); - let handle_a = assets.add(A); + let handle_a = app.world_mut().spawn_asset(A); let dynamic_handle_a = handle_a.to_dynamic(); let reflected_handle_a = handle_a.as_partial_reflect(); @@ -944,13 +1178,11 @@ mod tests { let mut app = create_app().0; app.init_asset::().init_asset::(); - let mut assets_a = app.world_mut().resource_mut::>(); - let handle_a = assets_a.add(A); + let handle_a = app.world_mut().spawn_asset(A); let reflected_handle_a = handle_a.as_partial_reflect(); - let mut assets_b = app.world_mut().resource_mut::>(); - let mut handle_b = assets_b.add(B); + let mut handle_b = app.world_mut().spawn_asset(B); assert!( handle_b.try_apply(reflected_handle_a).is_err(), "Handle should not be applicable to Handle" @@ -965,15 +1197,14 @@ mod tests { let mut app = create_app().0; app.init_asset::(); - let mut assets = app.world_mut().resource_mut::>(); - let handle_1 = assets.add(A(1)); + let handle_1 = app.world_mut().spawn_asset(A(1)); let reflected_handle_1 = handle_1.as_partial_reflect(); let handle_1_from_reflect: Handle = FromReflect::from_reflect(reflected_handle_1).unwrap(); assert_eq!(handle_1, handle_1_from_reflect); - let mut handle_2 = assets.add(A(2)); + let mut handle_2 = app.world_mut().spawn_asset(A(2)); assert_ne!(handle_1, handle_2); handle_2.try_apply(reflected_handle_1).unwrap(); assert_eq!(handle_1, handle_2); diff --git a/crates/bevy_asset/src/id.rs b/crates/bevy_asset/src/id.rs index f4db2cef4d15c..53a2746547922 100644 --- a/crates/bevy_asset/src/id.rs +++ b/crates/bevy_asset/src/id.rs @@ -1,16 +1,38 @@ -use crate::{Asset, AssetIndex, Handle, UntypedHandle}; -use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - use core::{ any::TypeId, fmt::{Debug, Display}, hash::Hash, marker::PhantomData, }; -use derive_more::derive::From; + +use bevy_ecs::entity::Entity; +use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use derive_more::{Display, From}; use thiserror::Error; +use uuid::Uuid; + +use crate::Asset; + +/// A wrapper around an [`Entity`] indicating this refers to an asset. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Display, Reflect)] +pub struct AssetEntity(Entity); + +impl AssetEntity { + /// Creates a new instance for `entity`. + /// + /// No checking is performed that this entity actually refers to an asset. It is up to the + /// caller to ensure this is an asset. + #[inline] + pub fn new_unchecked(entity: Entity) -> Self { + Self(entity) + } + + /// Returns the entity being referenced. + #[inline] + pub fn raw_entity(&self) -> Entity { + self.0 + } +} /// A unique runtime-only identifier for an [`Asset`]. This is cheap to [`Copy`]/[`Clone`] and is not directly tied to the /// lifetime of the Asset. This means it _can_ point to an [`Asset`] that no longer exists. @@ -18,25 +40,25 @@ use thiserror::Error; /// For an identifier tied to the lifetime of an asset, see [`Handle`](`crate::Handle`). /// /// For an "untyped" / "generic-less" id, see [`UntypedAssetId`]. -#[derive(Reflect, Serialize, Deserialize, From)] +#[derive(Reflect, From)] #[reflect(Clone, Default, Debug, PartialEq, Hash)] pub enum AssetId { - /// A small / efficient runtime identifier that can be used to efficiently look up an asset stored in [`Assets`]. This is - /// the "default" identifier used for assets. The alternative(s) (ex: [`AssetId::Uuid`]) will only be used if assets are + /// The entity on which the asset data is stored. This is the "default" identifier used for + /// assets. The alternative(s) (ex: [`AssetId::Uuid`]) will only be used if assets are /// explicitly registered that way. - /// - /// [`Assets`]: crate::Assets - Index { - /// The unstable, opaque index of the asset. - index: AssetIndex, + Entity { + /// The entity which (will) store the data. + entity: AssetEntity, /// A marker to store the type information of the asset. #[reflect(ignore, clone)] marker: PhantomData A>, }, - /// A stable-across-runs / const asset identifier. This will only be used if an asset is explicitly registered in [`Assets`] - /// with one. + /// A stable-across-runs / const asset identifier. This will only be used if an asset is + /// explicitly registered with one (through [`DirectAssetAccessExt::spawn_uuid_asset`] or + /// [`AssetCommands::spawn_uuid_asset`]). /// - /// [`Assets`]: crate::Assets + /// [`DirectAssetAccessExt::spawn_uuid_asset`]: crate::DirectAssetAccessExt::spawn_uuid_asset + /// [`AssetCommands::spawn_uuid_asset`]: crate::AssetCommands::spawn_uuid_asset Uuid { /// The UUID provided during asset registration. uuid: Uuid, @@ -44,12 +66,12 @@ pub enum AssetId { } impl AssetId { - /// The UUID for the default [`AssetId`]. It is valid to assign a value to this in [`Assets`](crate::Assets) - /// and by convention (where appropriate) assets should support this pattern. + /// The UUID for the default [`AssetId`]. It is valid to assign a value to this and by + /// convention (where appropriate) assets should support this pattern. pub const DEFAULT_UUID: Uuid = Uuid::from_u128(200809721996911295814598172825939264631); - /// This asset id _should_ never be valid. Assigning a value to this in [`Assets`](crate::Assets) will - /// produce undefined behavior, so don't do it! + /// This asset id _should_ never be valid. Assigning a value to this will produce undefined + /// behavior, so don't do it! #[deprecated( since = "0.20.0", note = "Use `Option` if possible. `AssetId::default` may also work, but note that the default can map to a valid asset." @@ -69,6 +91,24 @@ impl AssetId { } } + /// Returns the entity if this is an entity ID. + #[inline] + pub fn entity(&self) -> Option { + match self { + Self::Entity { entity, .. } => Some(*entity), + Self::Uuid { .. } => None, + } + } + + /// Returns the UUID if this is a UUID ID. + #[inline] + pub fn uuid(&self) -> Option { + match self { + Self::Uuid { uuid } => Some(*uuid), + Self::Entity { .. } => None, + } + } + /// Converts this to an "untyped" / "generic-less" [`Asset`] identifier that stores the type information /// _inside_ the [`UntypedAssetId`]. #[inline] @@ -79,7 +119,7 @@ impl AssetId { #[inline] fn internal(self) -> InternalAssetId { match self { - AssetId::Index { index, .. } => InternalAssetId::Index(index), + AssetId::Entity { entity, .. } => InternalAssetId::Entity(entity), AssetId::Uuid { uuid } => InternalAssetId::Uuid(uuid), } } @@ -110,13 +150,12 @@ impl Display for AssetId { impl Debug for AssetId { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { - AssetId::Index { index, .. } => { + AssetId::Entity { entity, .. } => { write!( f, - "AssetId<{}>{{ index: {}, generation: {}}}", + "AssetId<{}>{{ entity: {} }}", core::any::type_name::(), - index.index, - index.generation + *entity, ) } AssetId::Uuid { uuid } => { @@ -160,11 +199,11 @@ impl Ord for AssetId { } } -impl From for AssetId { +impl From for AssetId { #[inline] - fn from(value: AssetIndex) -> Self { - Self::Index { - index: value, + fn from(value: AssetEntity) -> Self { + Self::Entity { + entity: value, marker: PhantomData, } } @@ -175,21 +214,17 @@ impl From for AssetId { /// across asset types together and enables comparisons between them. #[derive(Debug, Copy, Clone, Reflect)] pub enum UntypedAssetId { - /// A small / efficient runtime identifier that can be used to efficiently look up an asset stored in [`Assets`]. This is - /// the "default" identifier used for assets. The alternative(s) (ex: [`UntypedAssetId::Uuid`]) will only be used if assets are - /// explicitly registered that way. - /// - /// [`Assets`]: crate::Assets - Index { + /// The entity on which the data is stored. This is the "default" identifier used for assets. + /// The alternative(s) (ex: [`UntypedAssetId::Uuid`]) will only be used if assets are explicitly + /// registered that way. + Entity { /// An identifier that records the underlying asset type. type_id: TypeId, - /// The unstable, opaque index of the asset. - index: AssetIndex, + /// The entity storing this asset. + entity: AssetEntity, }, - /// A stable-across-runs / const asset identifier. This will only be used if an asset is explicitly registered in [`Assets`] - /// with one. - /// - /// [`Assets`]: crate::Assets + /// A stable-across-runs / const asset identifier. This will only be used if an asset is + /// explicitly registered with one. Uuid { /// An identifier that records the underlying asset type. type_id: TypeId, @@ -199,14 +234,32 @@ pub enum UntypedAssetId { } impl UntypedAssetId { + /// Returns the entity if this is an entity ID. + #[inline] + pub fn entity(&self) -> Option { + match self { + Self::Entity { entity, .. } => Some(*entity), + Self::Uuid { .. } => None, + } + } + + /// Returns the UUID if this is a UUID ID. + #[inline] + pub fn uuid(&self) -> Option { + match self { + Self::Uuid { uuid, .. } => Some(*uuid), + Self::Entity { .. } => None, + } + } + /// Converts this to a "typed" [`AssetId`] without checking the stored type to see if it matches the target `A` [`Asset`] type. /// This should only be called if you are _absolutely certain_ the asset type matches the stored type. And even then, you should /// consider using [`UntypedAssetId::typed_debug_checked`] instead. #[inline] pub fn typed_unchecked(self) -> AssetId { match self { - UntypedAssetId::Index { index, .. } => AssetId::Index { - index, + UntypedAssetId::Entity { entity, .. } => AssetId::Entity { + entity, marker: PhantomData, }, UntypedAssetId::Uuid { uuid, .. } => AssetId::Uuid { uuid }, @@ -257,7 +310,7 @@ impl UntypedAssetId { #[inline] pub fn type_id(&self) -> TypeId { match self { - UntypedAssetId::Index { type_id, .. } | UntypedAssetId::Uuid { type_id, .. } => { + UntypedAssetId::Entity { type_id, .. } | UntypedAssetId::Uuid { type_id, .. } => { *type_id } } @@ -266,7 +319,7 @@ impl UntypedAssetId { #[inline] fn internal(self) -> InternalAssetId { match self { - UntypedAssetId::Index { index, .. } => InternalAssetId::Index(index), + UntypedAssetId::Entity { entity, .. } => InternalAssetId::Entity(entity), UntypedAssetId::Uuid { uuid, .. } => InternalAssetId::Uuid(uuid), } } @@ -276,11 +329,8 @@ impl Display for UntypedAssetId { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let mut writer = f.debug_struct("UntypedAssetId"); match self { - UntypedAssetId::Index { index, type_id } => { - writer - .field("type_id", type_id) - .field("index", &index.index) - .field("generation", &index.generation); + UntypedAssetId::Entity { entity, type_id } => { + writer.field("type_id", type_id).field("entity", &entity); } UntypedAssetId::Uuid { uuid, type_id } => { writer.field("type_id", type_id).field("uuid", uuid); @@ -326,33 +376,10 @@ impl PartialOrd for UntypedAssetId { /// This is provided to make implementing traits easier for the many different asset ID types. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, PartialOrd, Ord, From)] enum InternalAssetId { - Index(AssetIndex), + Entity(AssetEntity), Uuid(Uuid), } -/// An asset index bundled with its (dynamic) type. -#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] -pub(crate) struct ErasedAssetIndex { - pub(crate) index: AssetIndex, - pub(crate) type_id: TypeId, -} - -impl ErasedAssetIndex { - pub(crate) fn new(index: AssetIndex, type_id: TypeId) -> Self { - Self { index, type_id } - } -} - -impl Display for ErasedAssetIndex { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("ErasedAssetIndex") - .field("type_id", &self.type_id) - .field("index", &self.index.index) - .field("generation", &self.index.generation) - .finish() - } -} - // Cross Operations impl PartialEq for AssetId { @@ -393,7 +420,7 @@ impl From> for UntypedAssetId { let type_id = TypeId::of::(); match value { - AssetId::Index { index, .. } => UntypedAssetId::Index { type_id, index }, + AssetId::Entity { entity, .. } => UntypedAssetId::Entity { type_id, entity }, AssetId::Uuid { uuid } => UntypedAssetId::Uuid { type_id, uuid }, } } @@ -408,10 +435,12 @@ impl TryFrom for AssetId { let expected = TypeId::of::(); match value { - UntypedAssetId::Index { index, type_id } if type_id == expected => Ok(AssetId::Index { - index, - marker: PhantomData, - }), + UntypedAssetId::Entity { entity, type_id } if type_id == expected => { + Ok(AssetId::Entity { + entity, + marker: PhantomData, + }) + } UntypedAssetId::Uuid { uuid, type_id } if type_id == expected => { Ok(AssetId::Uuid { uuid }) } @@ -420,52 +449,6 @@ impl TryFrom for AssetId { } } -impl TryFrom for ErasedAssetIndex { - type Error = UuidNotSupportedError; - - fn try_from(asset_id: UntypedAssetId) -> Result { - match asset_id { - UntypedAssetId::Index { type_id, index } => Ok(ErasedAssetIndex { index, type_id }), - UntypedAssetId::Uuid { .. } => Err(UuidNotSupportedError), - } - } -} - -impl TryFrom<&Handle> for ErasedAssetIndex { - type Error = UuidNotSupportedError; - - fn try_from(handle: &Handle) -> Result { - match handle { - Handle::Strong(handle) => Ok(Self::new(handle.index, handle.type_id)), - Handle::Uuid(..) => Err(UuidNotSupportedError), - } - } -} - -impl TryFrom<&UntypedHandle> for ErasedAssetIndex { - type Error = UuidNotSupportedError; - - fn try_from(handle: &UntypedHandle) -> Result { - match handle { - UntypedHandle::Strong(handle) => Ok(Self::new(handle.index, handle.type_id)), - UntypedHandle::Uuid { .. } => Err(UuidNotSupportedError), - } - } -} - -impl From for UntypedAssetId { - fn from(value: ErasedAssetIndex) -> Self { - Self::Index { - type_id: value.type_id, - index: value.index, - } - } -} - -#[derive(Error, Debug)] -#[error("Attempted to create a TypedAssetIndex from a Uuid")] -pub(crate) struct UuidNotSupportedError; - /// Errors preventing the conversion of to/from an [`UntypedAssetId`] and an [`AssetId`]. #[derive(Error, Debug, PartialEq, Clone)] #[non_exhaustive] diff --git a/crates/bevy_asset/src/io/embedded/mod.rs b/crates/bevy_asset/src/io/embedded/mod.rs index 41980e91abdd1..1699c5f1acd4e 100644 --- a/crates/bevy_asset/src/io/embedded/mod.rs +++ b/crates/bevy_asset/src/io/embedded/mod.rs @@ -385,20 +385,28 @@ pub fn watched_path(_source_file_path: &'static str, _asset_path: &'static str) #[macro_export] macro_rules! load_internal_asset { ($app: ident, $handle: expr, $path_str: expr, $loader: expr) => {{ - let mut assets = $app.world_mut().resource_mut::<$crate::Assets<_>>(); - assets.insert($handle.id(), ($loader)( + use $crate::{AssetId, DirectAssetAccessExt}; + let asset = ($loader)( ::core::include_str!($path_str), ::std::path::Path::new(::core::file!()) .parent() .unwrap() .join($path_str) - .to_string_lossy() - )).unwrap(); + .to_string_lossy()); + let world = $app.world_mut(); + match $handle.id() { + id @ AssetId::Entity { .. } => { + world.insert_asset(id, asset).unwrap(); + } + AssetId::Uuid { uuid } => { + world.spawn_uuid_asset(uuid, asset); + } + } }}; // we can't support params without variadic arguments, so internal assets with additional params can't be hot-reloaded ($app: ident, $handle: ident, $path_str: expr, $loader: expr $(, $param:expr)+) => {{ - let mut assets = $app.world_mut().resource_mut::<$crate::Assets<_>>(); - assets.insert($handle.id(), ($loader)( + use $crate::{AssetId, DirectAssetAccessExt}; + let asset = ($loader)( ::core::include_str!($path_str), ::std::path::Path::new(::core::file!()) .parent() @@ -406,7 +414,16 @@ macro_rules! load_internal_asset { .join($path_str) .to_string_lossy(), $($param),+ - )).unwrap(); + ); + let world = $app.world_mut(); + match $handle.id() { + id @ AssetId::Entity { .. } => { + world.insert_asset(id, asset).unwrap(); + } + AssetId::Uuid { uuid } => { + world.spawn_uuid_asset(uuid, asset); + } + } }}; } @@ -414,21 +431,25 @@ macro_rules! load_internal_asset { #[macro_export] macro_rules! load_internal_binary_asset { ($app: ident, $handle: expr, $path_str: expr, $loader: expr) => {{ - let mut assets = $app.world_mut().resource_mut::<$crate::Assets<_>>(); - assets - .insert( - $handle.id(), - ($loader)( - ::core::include_bytes!($path_str).as_ref(), - ::std::path::Path::new(::core::file!()) - .parent() - .unwrap() - .join($path_str) - .to_string_lossy() - .into(), - ), - ) - .unwrap(); + use $crate::{AssetId, DirectAssetAccessExt}; + let asset = ($loader)( + ::core::include_bytes!($path_str).as_ref(), + ::std::path::Path::new(::core::file!()) + .parent() + .unwrap() + .join($path_str) + .to_string_lossy() + .into(), + ); + let world = $app.world_mut(); + match $handle.id() { + id @ AssetId::Entity { .. } => { + world.insert_asset(id, asset).unwrap(); + } + AssetId::Uuid { uuid } => { + world.spawn_uuid_asset(uuid, asset); + } + } }}; } diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index a2b4bca3a6580..55ce175da8d15 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -10,9 +10,9 @@ //! then larger game worlds will need to load them from disk as needed, ideally without a loading screen. //! //! As is common in Rust, non-blocking asset loading is done using `async`, with background tasks used to load assets while the game is running. -//! Bevy coordinates these tasks using the [`AssetServer`] resource, storing each loaded asset in a strongly-typed [`Assets`] collection (also a resource). -//! [`Handle`]s serve as an id-based reference to entries in the [`Assets`] collection, allowing them to be cheaply shared between systems, -//! and providing a way to initialize objects (generally entities) before the required assets are loaded. +//! Bevy coordinates these tasks using the [`AssetServer`] resource, storing each loaded asset in an entity. +//! [`Handle`]s serve as a reference to these entities, allowing them to be cheaply shared between systems, +//! and providing a way to initialize objects (generally components) before the required assets are loaded. //! In short: [`Handle`]s are not the assets themselves, they just tell how to look them up! //! //! ## Loading assets @@ -37,8 +37,8 @@ //! If we later want to change the asset data a given component uses (such as changing an entity's material), we have three options: //! //! 1. Change the handle stored on the responsible component to the handle of a different asset -//! 2. Despawn the entity and spawn a new one with the new asset data. -//! 3. Use the [`Assets`] collection to directly modify the current handle's asset data +//! 2. Despawn the entity and spawn a new one with the new asset handle. +//! 3. Use the [`AssetsMut`] system param to directly modify the current handle's asset data //! //! The first option is the most common: just query for the component that holds the handle, and mutate it, pointing to the new asset. //! Check how the handle was passed in to the entity when it was spawned: if a mesh-related component required a handle to a mesh asset, @@ -56,7 +56,7 @@ //! //! Bevy supports asset hot reloading, allowing you to change assets on disk and see the changes reflected in your game without restarting. //! When enabled, any changes to the underlying asset file will be detected by the [`AssetServer`], which will then reload the asset, -//! mutating the asset data in the [`Assets`] collection and thus updating all entities that use the asset. +//! mutating the asset data on its entity and thus updating all other entities that use the asset. //! While it has limited uses in published games, it is very useful when developing, as it allows you to iterate quickly. //! //! To enable asset hot reloading on desktop platforms, enable `bevy`'s `file_watcher` cargo feature. @@ -65,22 +65,22 @@ //! # Procedural asset creation //! //! Not all assets are loaded from disk: some are generated at runtime, such as procedural materials, sounds or even levels. -//! After creating an item of a type that implements [`Asset`], you can add it to the [`Assets`] collection using [`Assets::add`]. -//! Once in the asset collection, this data can be operated on like any other asset. +//! After creating an item of a type that implements [`Asset`], you can add it to the world using [`AssetCommands::spawn_asset`]. +//! Once spawned, this data can be operated on like any other asset. //! //! Note that, unlike assets loaded from a file path, no general mechanism currently exists to deduplicate procedural assets: -//! calling [`Assets::add`] for every entity that needs the asset will create a new copy of the asset for each entity, +//! calling [`AssetCommands::spawn_asset`] for every entity that needs the asset will create a new copy of the asset for each entity, //! quickly consuming memory. //! //! ## Handles and reference counting //! -//! [`Handle`] (or their untyped counterpart [`UntypedHandle`]) are used to reference assets in the [`Assets`] collection, +//! [`Handle`] (or their untyped counterpart [`UntypedHandle`]) are used to reference assets, //! and are the primary way to interact with assets in Bevy. //! As a user, you'll be working with handles a lot! //! //! The most important thing to know about handles is that they are reference counted: when you clone a handle, you're incrementing a reference count. //! When the object holding the handle is dropped (generally because an entity was despawned), the reference count is decremented. -//! When the reference count hits zero, the asset it references is removed from the [`Assets`] collection. +//! When the reference count hits zero, the asset it references is despawned. //! //! This reference counting is a simple, largely automatic way to avoid holding onto memory for game objects that are no longer in use. //! However, it can lead to surprising behavior if you're not careful! @@ -166,8 +166,9 @@ pub mod prelude { #[doc(hidden)] pub use crate::{ - asset_value, Asset, AssetApp, AssetEvent, AssetId, AssetMode, AssetPlugin, AssetServer, - Assets, DirectAssetAccessExt, Handle, UntypedHandle, + asset_value, Asset, AssetApp, AssetCommands, AssetEvent, AssetId, AssetMode, AssetPlugin, + AssetServer, AssetServerAccessExt, Assets, AssetsMut, DirectAssetAccessExt, Handle, + UntypedHandle, WorldAssetCommandsExt, }; } @@ -183,11 +184,11 @@ mod path; mod reflect; mod render_asset; mod server; +mod uuid_map; pub use assets::*; pub use bevy_asset_macros::{Asset, VisitAssetDependencies}; -use bevy_diagnostic::{Diagnostic, DiagnosticsStore, RegisterDiagnostic}; -pub use direct_access_ext::DirectAssetAccessExt; +pub use direct_access_ext::*; pub use event::*; pub use folder::*; pub use futures_lite::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; @@ -199,6 +200,7 @@ pub use path::*; pub use reflect::*; pub use render_asset::*; pub use server::*; +pub use uuid_map::*; pub use uuid; @@ -214,6 +216,7 @@ use alloc::{ vec::Vec, }; use bevy_app::{App, Plugin, PostUpdate, PreUpdate}; +use bevy_diagnostic::{Diagnostic, DiagnosticsStore, RegisterDiagnostic}; use bevy_ecs::{prelude::Component, schedule::common_conditions::resource_exists}; use bevy_ecs::{ reflect::AppTypeRegistry, @@ -222,7 +225,6 @@ use bevy_ecs::{ }; use bevy_platform::collections::{HashMap, HashSet}; use bevy_reflect::{FromReflect, GetTypeRegistration, Reflect, TypePath}; -use core::any::TypeId; use tracing::error; /// Provides "asset" loading and processing functionality. An [`Asset`] is a "runtime value" that is loaded from an [`AssetSource`], @@ -351,7 +353,13 @@ impl AssetPlugin { impl Plugin for AssetPlugin { fn build(&self, app: &mut App) { + app.add_plugins(MinimalAssetPlugin); + let embedded = EmbeddedAssetRegistry::default(); + + let handle_provider = app.world().resource::().clone(); + let uuid_map = app.world().resource::().clone(); + { let mut sources = app .world_mut() @@ -364,6 +372,10 @@ impl Plugin for AssetPlugin { embedded.register_source(&mut sources); } { + let remote_allocator = app + .world_mut() + .entity_allocator_mut() + .build_remote_allocator(); let watch = self .watch_for_changes_override .unwrap_or(cfg!(feature = "watch")); @@ -373,6 +385,9 @@ impl Plugin for AssetPlugin { let sources = builders.build_sources(watch, false); app.insert_resource(AssetServer::new_with_meta_check( + remote_allocator, + handle_provider.clone(), + uuid_map, Arc::new(sources), AssetServerMode::Unprocessed, self.meta_check.clone(), @@ -387,9 +402,12 @@ impl Plugin for AssetPlugin { if use_asset_processor { let mut builders = app.world_mut().resource_mut::(); let (processor, sources) = AssetProcessor::new(&mut builders, watch); - // the main asset server shares loaders with the processor asset server app.insert_resource(AssetServer::new_with_loaders( + remote_allocator, + handle_provider.clone(), + uuid_map, sources, + // the main asset server shares loaders with the processor asset server processor.server().data.loaders.clone(), AssetServerMode::Processed, AssetMetaCheck::Always, @@ -402,6 +420,9 @@ impl Plugin for AssetPlugin { let mut builders = app.world_mut().resource_mut::(); let sources = builders.build_sources(false, watch); app.insert_resource(AssetServer::new_with_meta_check( + remote_allocator, + handle_provider.clone(), + uuid_map, Arc::new(sources), AssetServerMode::Processed, AssetMetaCheck::Always, @@ -417,18 +438,16 @@ impl Plugin for AssetPlugin { .init_asset::() .init_asset::<()>() .add_message::() - .configure_sets( - PreUpdate, - AssetTrackingSystems.after(handle_internal_asset_events), - ) // `handle_internal_asset_events` requires the use of `&mut World`, // and as a result has ambiguous system ordering with all other systems in `PreUpdate`. - // This is virtually never a real problem: asset loading is async and so anything that interacts directly with it - // needs to be robust to stochastic delays anyways. + // This is virtually never a real problem: asset loading is async and so anything that + // interacts directly with it needs to be robust to stochastic delays anyways. .add_systems( PreUpdate, ( - handle_internal_asset_events.ambiguous_with_all(), + handle_internal_asset_events + .ambiguous_with_all() + .before(despawn_unused_assets), // TODO: Remove the run condition and use `If` once // https://github.com/bevyengine/bevy/issues/21549 is resolved. publish_asset_server_diagnostics.run_if(resource_exists::), @@ -440,7 +459,7 @@ impl Plugin for AssetPlugin { } /// Declares that this type is an asset, -/// which can be loaded and managed by the [`AssetServer`] and stored in [`Assets`] collections. +/// which can be loaded and managed by the [`AssetServer`] and stored in [`AssetData`]. /// /// Generally, assets are large, complex, and/or expensive to load from disk, and are often authored by artists or designers. /// @@ -453,9 +472,8 @@ impl Plugin for AssetPlugin { )] pub trait Asset: VisitAssetDependencies + TypePath + Send + Sync + 'static {} -/// A trait for components that can be used as asset identifiers, e.g. handle wrappers. +/// A trait for components that can be used as asset references, e.g. handle wrappers. pub trait AsAssetId: Component { - /// The underlying asset type. type Asset: Asset; /// Retrieves the asset id from this component. @@ -468,29 +486,29 @@ pub trait AsAssetId: Component { /// Note that this trait is automatically implemented when deriving [`Asset`]. pub trait VisitAssetDependencies { /// Apply the `visit` closure to every asset dependency. - fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)); + fn visit_dependencies(&self, visit: &mut impl FnMut(AssetEntity)); } impl VisitAssetDependencies for Handle { - fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { - visit(self.id().untyped()); + fn visit_dependencies(&self, visit: &mut impl FnMut(AssetEntity)) { + self.entity().map(visit); } } impl VisitAssetDependencies for UntypedHandle { - fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { - visit(self.id()); + fn visit_dependencies(&self, visit: &mut impl FnMut(AssetEntity)) { + self.entity().map(visit); } } impl VisitAssetDependencies for UntypedAssetId { - fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { - visit(*self); + fn visit_dependencies(&self, visit: &mut impl FnMut(AssetEntity)) { + self.entity().map(visit); } } impl VisitAssetDependencies for Option { - fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { + fn visit_dependencies(&self, visit: &mut impl FnMut(AssetEntity)) { if let Some(dependency) = self { dependency.visit_dependencies(visit); } @@ -498,7 +516,7 @@ impl VisitAssetDependencies for Option { } impl VisitAssetDependencies for [V; N] { - fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { + fn visit_dependencies(&self, visit: &mut impl FnMut(AssetEntity)) { for dependency in self { dependency.visit_dependencies(visit); } @@ -506,7 +524,7 @@ impl VisitAssetDependencies for [V; N } impl VisitAssetDependencies for [V] { - fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { + fn visit_dependencies(&self, visit: &mut impl FnMut(AssetEntity)) { for dependency in self { dependency.visit_dependencies(visit); } @@ -514,13 +532,13 @@ impl VisitAssetDependencies for [V] { } impl VisitAssetDependencies for Box { - fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { + fn visit_dependencies(&self, visit: &mut impl FnMut(AssetEntity)) { self.as_ref().visit_dependencies(visit); } } impl VisitAssetDependencies for Vec { - fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { + fn visit_dependencies(&self, visit: &mut impl FnMut(AssetEntity)) { for dependency in self { dependency.visit_dependencies(visit); } @@ -528,7 +546,7 @@ impl VisitAssetDependencies for Vec { } impl VisitAssetDependencies for VecDeque { - fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { + fn visit_dependencies(&self, visit: &mut impl FnMut(AssetEntity)) { for dependency in self { dependency.visit_dependencies(visit); } @@ -536,7 +554,7 @@ impl VisitAssetDependencies for VecDeque { } impl VisitAssetDependencies for HashSet { - fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { + fn visit_dependencies(&self, visit: &mut impl FnMut(AssetEntity)) { for dependency in self { dependency.visit_dependencies(visit); } @@ -544,7 +562,7 @@ impl VisitAssetDependencies for HashSet { } impl VisitAssetDependencies for HashMap { - fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { + fn visit_dependencies(&self, visit: &mut impl FnMut(AssetEntity)) { for dependency in self.values() { dependency.visit_dependencies(visit); } @@ -552,7 +570,7 @@ impl VisitAssetDependencies for HashMap { } impl VisitAssetDependencies for BTreeSet { - fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { + fn visit_dependencies(&self, visit: &mut impl FnMut(AssetEntity)) { for dependency in self { dependency.visit_dependencies(visit); } @@ -560,7 +578,7 @@ impl VisitAssetDependencies for BTreeSet { } impl VisitAssetDependencies for BTreeMap { - fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)) { + fn visit_dependencies(&self, visit: &mut impl FnMut(AssetEntity)) { for dependency in self.values() { dependency.visit_dependencies(visit); } @@ -588,9 +606,9 @@ pub trait AssetApp { fn init_asset_loader(&mut self) -> &mut Self; /// Initializes the given [`Asset`] in the [`App`] by: /// * Registering the [`Asset`] in the [`AssetServer`] - /// * Initializing the [`AssetEvent`] resource for the [`Asset`] + /// * Initializing the [`AssetEvent`] message for the [`Asset`] /// * Adding other relevant systems and resources for the [`Asset`] - /// * Ignoring schedule ambiguities in [`Assets`] resource. Any time a system takes + /// * Ignoring schedule ambiguities in [`AssetData`]. Any time a system takes /// mutable access to this resource this causes a conflict, but they rarely actually /// modify the same underlying asset. fn init_asset(&mut self) -> &mut Self; @@ -654,37 +672,26 @@ impl AssetApp for App { } fn init_asset(&mut self) -> &mut Self { - let assets = Assets::::default(); - self.world() - .resource::() - .register_asset(&assets); - if self.world().contains_resource::() { - let processor = self.world().resource::(); - // The processor should have its own handle provider separate from the Asset storage - // to ensure the id spaces are entirely separate. Not _strictly_ necessary, but - // desirable. - processor - .server() - .register_handle_provider(AssetHandleProvider::new( - TypeId::of::(), - Arc::new(AssetIndexAllocator::default()), - )); - } - self.insert_resource(assets) - .allow_ambiguous_resource::>() + self.world_mut() + .resource_mut::() + .register::(); + self.allow_ambiguous_component::>() .add_message::>() .add_message::>() .register_type::>() .add_systems( PostUpdate, - Assets::::asset_events - .run_if(Assets::::asset_events_condition) - .in_set(AssetEventSystems), - ) - .add_systems( - PreUpdate, - Assets::::track_assets.in_set(AssetTrackingSystems), - ) + write_modified_asset_events::.in_set(AssetEventSystems), + ); + + if let Some(asset_server) = self.world().get_resource::() { + asset_server.register_asset::(); + } + if let Some(processor) = self.world().get_resource::() { + processor.server().register_asset::(); + } + + self } fn register_asset_reflect(&mut self) -> &mut Self @@ -715,11 +722,36 @@ impl AssetApp for App { } } -/// A system set that holds all "track asset" operations. +/// A minimal form of [`AssetPlugin`] necessary for creating and accessing assets. +/// +/// Notably this includes none of the asset loading resources. In general, users should always +/// prefer the [`AssetPlugin`]. However this is provided to allow for the basic functionality of +/// assets, which can be useful for tests. +pub struct MinimalAssetPlugin; + +impl Plugin for MinimalAssetPlugin { + fn build(&self, app: &mut App) { + let handle_provider = AssetHandleProvider::new(); + app.insert_resource(handle_provider) + .init_resource::() + .init_resource::() + .add_systems( + PreUpdate, + // `despawn_unused_assets` requires `&mut World`, but since these assets are unused, + // it's unlikely systems actually needs explicit ordering here. So we can ignore + // ambiguities. + despawn_unused_assets + .ambiguous_with_all() + .in_set(AssetTrackingSystems), + ); + } +} + +/// A system set that holds asset tracking operations. #[derive(SystemSet, Hash, Debug, PartialEq, Eq, Clone)] pub struct AssetTrackingSystems; -/// A system set where events accumulated in [`Assets`] are applied to the [`AssetEvent`] [`Messages`] resource. +/// A system set where events accumulated in [`Asset`]s are applied to the [`AssetEvent`] [`Messages`] resource. /// /// [`Messages`]: bevy_ecs::message::Messages #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] @@ -738,7 +770,7 @@ mod tests { }, loader::{AssetLoader, LoadContext}, Asset, AssetApp, AssetEvent, AssetId, AssetLoadError, AssetLoadFailedEvent, AssetPath, - AssetPlugin, AssetServer, Assets, InvalidGenerationError, LoadState, LoadedAsset, + AssetPlugin, AssetServer, Assets, AssetsMut, DirectAssetAccessExt, LoadState, LoadedAsset, UnapprovedPathMode, UntypedHandle, VisitAssetDependencies, WriteDefaultMetaError, }; use alloc::{ @@ -756,6 +788,7 @@ mod tests { message::MessageCursor, prelude::*, schedule::{LogLevel, ScheduleBuildSettings}, + system::SystemState, }; use bevy_platform::{ collections::{HashMap, HashSet}, @@ -987,7 +1020,7 @@ mod tests { const LARGE_ITERATION_COUNT: usize = 10000; fn get(world: &World, id: AssetId) -> Option<&A> { - world.resource::>().get(id) + world.get_asset(id) } fn get_started_load_count(world: &World) -> usize { @@ -1265,8 +1298,7 @@ mod tests { assert_eq!(get_started_load_count(app.world()), 6); { - let mut texts = app.world_mut().resource_mut::>(); - let mut a = texts.get_mut(a_id).unwrap(); + let mut a = app.world_mut().get_asset_mut(a_id).unwrap(); a.text = "Changed".to_string(); } @@ -1274,14 +1306,22 @@ mod tests { app.update(); assert_eq!( - app.world().resource::>().len(), + { + SystemState::>::new(app.world_mut()) + .get(app.world_mut()) + .unwrap() + .count() + }, 0, "CoolText asset entities should be despawned when no more handles exist" ); app.update(); // this requires a second update because the parent asset was freed in the previous app.update() assert_eq!( - app.world().resource::>().len(), + SystemState::>::new(app.world_mut()) + .get(app.world_mut()) + .unwrap() + .count(), 0, "SubText asset entities should be despawned when no more handles exist" ); @@ -1289,15 +1329,18 @@ mod tests { let id_results = app.world_mut().remove_resource::().unwrap(); let expected_events = vec![ AssetEvent::Added { id: a_id }, - AssetEvent::LoadedWithDependencies { + AssetEvent::Added { id: id_results.b_id, }, - AssetEvent::Added { + AssetEvent::LoadedWithDependencies { id: id_results.b_id, }, AssetEvent::Added { id: id_results.c_id, }, + AssetEvent::Added { + id: id_results.d_id, + }, AssetEvent::LoadedWithDependencies { id: id_results.d_id, }, @@ -1305,10 +1348,6 @@ mod tests { id: id_results.c_id, }, AssetEvent::LoadedWithDependencies { id: a_id }, - AssetEvent::Added { - id: id_results.d_id, - }, - AssetEvent::Modified { id: a_id }, AssetEvent::Unused { id: a_id }, AssetEvent::Removed { id: a_id }, AssetEvent::Unused { @@ -1594,15 +1633,16 @@ mod tests { let id = { let handle = { - let mut texts = app.world_mut().resource_mut::>(); - let handle = texts.add(CoolText::default()); - texts.get_strong_handle(handle.id()).unwrap() + let handle = app.world_mut().spawn_asset(CoolText::default()); + app.world() + .get_asset_strong_handle::(handle.entity().unwrap()) + .unwrap() }; app.update(); { - let text = app.world().resource::>().get(&handle); + let text = app.world().get_asset(handle.id()); assert!(text.is_some()); } handle.id() @@ -1610,7 +1650,7 @@ mod tests { // handle is dropped app.update(); assert!( - app.world().resource::>().get(id).is_none(), + app.world().get_asset(id).is_none(), "asset has no handles, so it should have been dropped last update" ); } @@ -1633,24 +1673,17 @@ mod tests { let empty = "".to_string(); let id = { - let handle = { - let mut texts = app.world_mut().resource_mut::>(); - texts.add(CoolText { - text: hello.clone(), - embedded: empty.clone(), - dependencies: vec![], - sub_texts: Vec::new(), - }) - }; + let handle = app.world_mut().spawn_asset(CoolText { + text: hello.clone(), + embedded: empty.clone(), + dependencies: vec![], + sub_texts: Vec::new(), + }); app.update(); { - let text = app - .world() - .resource::>() - .get(&handle) - .unwrap(); + let text = app.world().get_asset(handle.id()).unwrap(); assert_eq!(text.text, hello); } handle.id() @@ -1658,7 +1691,7 @@ mod tests { // handle is dropped app.update(); assert!( - app.world().resource::>().get(id).is_none(), + app.world().get_asset(id).is_none(), "asset has no handles, so it should have been dropped last update" ); // remove event is emitted @@ -1708,6 +1741,9 @@ mod tests { continue; } let expected_events = vec![ + AssetEvent::Added { + id: dep_handle.id(), + }, AssetEvent::LoadedWithDependencies { id: dep_handle.id(), }, @@ -1718,13 +1754,6 @@ mod tests { } assert_eq!(get_started_load_count(app.world()), 1); - - app.update(); - let events = core::mem::take(&mut app.world_mut().resource_mut::().0); - let expected_events = vec![AssetEvent::Added { - id: dep_handle.id(), - }]; - assert_eq!(events, expected_events); } #[test] @@ -1785,13 +1814,11 @@ mod tests { run_app_until(&mut app, |world| { let events = world.resource::>>(); let asset_server = world.resource::(); - let loaded_folders = world.resource::>(); - let cool_texts = world.resource::>(); for event in cursor.read(events) { if let AssetEvent::LoadedWithDependencies { id } = event && *id == handle.id() { - let loaded_folder = loaded_folders.get(&handle).unwrap(); + let loaded_folder = world.get_asset(handle.id()).unwrap(); let a_handle: Handle = asset_server.get_handle("text/a.cool.ron").unwrap(); let c_handle: Handle = @@ -1810,9 +1837,9 @@ mod tests { assert!(found_c); assert_eq!(loaded_folder.handles.len(), 2); - let a_text = cool_texts.get(&a_handle).unwrap(); - let b_text = cool_texts.get(&a_text.dependencies[0]).unwrap(); - let c_text = cool_texts.get(&c_handle).unwrap(); + let a_text = world.get_asset(a_handle.id()).unwrap(); + let b_text = world.get_asset(a_text.dependencies[0].id()).unwrap(); + let c_text = world.get_asset(c_handle.id()).unwrap(); assert_eq!("a", a_text.text); assert_eq!("b", b_text.text); @@ -1954,8 +1981,7 @@ mod tests { match tracker.finished_asset { Some(asset_id) => { assert_eq!(asset_id, a_id); - let assets = world.resource::>(); - let result = assets.get(asset_id).unwrap(); + let result = world.get_asset(asset_id).unwrap(); assert_eq!(result.text, "a"); Some(()) } @@ -1969,7 +1995,7 @@ mod tests { let mut app = create_app().0; app.init_asset::(); - fn uses_assets(_asset: ResMut>) {} + fn uses_assets(_asset: AssetsMut) {} app.add_systems(Update, (uses_assets, uses_assets)); app.edit_schedule(Update, |s| { s.set_build_settings(ScheduleBuildSettings { @@ -2204,35 +2230,6 @@ mod tests { run_app_until(&mut app, |_| asset_server.is_loaded(&handle).then_some(())); } - #[test] - fn insert_dropped_handle_returns_error() { - let mut app = create_app().0; - - app.init_asset::(); - - let handle = app.world().resource::>().reserve_handle(); - // We still have the asset ID, but we've dropped the handle so the asset is no longer live. - let asset_id = handle.id(); - drop(handle); - - // Allow `Assets` to detect the dropped handle. - app.world_mut() - .run_system_cached(Assets::::track_assets) - .unwrap(); - - let AssetId::Index { index, .. } = asset_id else { - unreachable!("Reserving a handle always produces an index"); - }; - - // Try to insert an asset into the dropped handle's spot. This should not panic. - assert_eq!( - app.world_mut() - .resource_mut::>() - .insert(asset_id, TestAsset), - Err(InvalidGenerationError::Removed { index }) - ); - } - /// A loader that notifies a sender when the loader has started, and blocks on a receiver to /// simulate a long asset loader. // Note: we can't just use the GatedReader, since currently we hold the handle until after @@ -2445,8 +2442,8 @@ mod tests { assert_eq!( messages, [ - AssetEvent::LoadedWithDependencies { id: handle.id() }, AssetEvent::Added { id: handle.id() }, + AssetEvent::LoadedWithDependencies { id: handle.id() }, ] ); Some(()) @@ -2521,8 +2518,8 @@ mod tests { assert_eq!( messages, [ + AssetEvent::Added { id: handle.id() }, AssetEvent::LoadedWithDependencies { id: handle.id() }, - AssetEvent::Added { id: handle.id() } ] ); Some(()) @@ -2597,8 +2594,8 @@ mod tests { run_app_until(&mut app, |world| { let (Some(asset_1), Some(asset_2)) = ( - world.resource::>().get(&handle_1), - world.resource::>().get(&handle_2), + world.get_asset(handle_1.id()), + world.get_asset(handle_2.id()), ) else { return None; }; @@ -2698,40 +2695,28 @@ mod tests { // Wait for the asset to load. run_app_until(&mut app, |world| { - world - .resource::>() - .get(&original_handle) - .map(|_| ()) + world.get_asset(original_handle.id()).map(|_| ()) }); assert_eq!(get_started_load_count(app.world()), 1); // Get a new strong handle from the original handle's ID. let new_handle = app - .world_mut() - .resource_mut::>() - .get_strong_handle(original_handle.id()) + .world() + .get_asset_strong_handle::(original_handle.entity().unwrap()) .unwrap(); // Drop the original handle. This should still leave the asset alive. drop(original_handle); app.update(); - assert!(app - .world() - .resource::>() - .get(&new_handle) - .is_some()); + assert!(app.world().get_asset(new_handle.id()).is_some()); let _other_handle: Handle = asset_server.load("test.txt"); app.update(); // The asset server should **not** have started a new load, since the asset is still alive. - // Due to https://github.com/bevyengine/bevy/issues/20651, we do get a second load. Once - // #20651 is fixed, we should swap these asserts. - // - // assert_eq!(get_started_load_count(app.world()), 1); - assert_eq!(get_started_load_count(app.world()), 2); + assert_eq!(get_started_load_count(app.world()), 1); } #[test] @@ -2814,13 +2799,10 @@ mod tests { let immediate_handle: Handle = server.load("a.immediate"); run_app_until(&mut app, |world| { - let immediate_assets = world.resource::>(); - let immediate = immediate_assets.get(&immediate_handle)?; + let immediate = world.get_asset(immediate_handle.id())?; let test_asset_handle = immediate.0.clone(); - world - .resource::>() - .get(&test_asset_handle)?; + world.get_asset(test_asset_handle.id())?; // The immediate asset is loaded, and the asset it got from its immediate load is also // loaded. @@ -2948,8 +2930,7 @@ mod tests { let dep_handle: Handle = app .world() - .resource::>() - .get(&subasset_handle) + .get_asset(subasset_handle.id()) .unwrap() .dep .clone(); @@ -3267,11 +3248,7 @@ mod tests { .then_some(()) }); - let folder = app - .world() - .resource::>() - .get(&folder_handle) - .unwrap(); + let folder = app.world().get_asset(folder_handle.id()).unwrap(); assert_eq!(folder.handles.len(), 2); let mut handles = folder .handles @@ -3285,9 +3262,8 @@ mod tests { let abc_handle = handles[0].clone(); let def_handle = handles[1].clone(); - let cool_texts = app.world().resource::>(); - assert_eq!(cool_texts.get(&abc_handle).unwrap().text, "abc"); - assert_eq!(cool_texts.get(&def_handle).unwrap().text, "def"); + assert_eq!(app.world().get_asset(abc_handle.id()).unwrap().text, "abc"); + assert_eq!(app.world().get_asset(def_handle.id()).unwrap().text, "def"); // Before doing any hot reloading stuff, clear out any AssetEvent messages. app.world_mut() @@ -3315,11 +3291,7 @@ mod tests { None }); - let folder = app - .world() - .resource::>() - .get(&folder_handle) - .unwrap(); + let folder = app.world().get_asset(folder_handle.id()).unwrap(); assert_eq!(folder.handles.len(), 3); let mut handles = folder .handles @@ -3337,9 +3309,17 @@ mod tests { assert_eq!(new_abc_handle, abc_handle); assert_eq!(new_def_handle, def_handle); - let cool_texts = app.world().resource::>(); - assert_eq!(cool_texts.get(&new_abc_handle).unwrap().text, "abc"); - assert_eq!(cool_texts.get(&new_def_handle).unwrap().text, "def"); - assert_eq!(cool_texts.get(&new_ghi_handle).unwrap().text, "ghi"); + assert_eq!( + app.world().get_asset(new_abc_handle.id()).unwrap().text, + "abc" + ); + assert_eq!( + app.world().get_asset(new_def_handle.id()).unwrap().text, + "def" + ); + assert_eq!( + app.world().get_asset(new_ghi_handle.id()).unwrap().text, + "ghi" + ); } } diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index 59566d89d6d24..70d970c5ffa57 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -1,9 +1,10 @@ use crate::{ + insert_asset, io::{AssetReaderError, MissingAssetSourceError, MissingProcessedAssetReaderError, Reader}, loader_builders::NestedLoadBuilder, meta::{AssetHash, AssetMeta, AssetMetaDyn, ProcessedInfo, ProcessedInfoMinimal, Settings}, path::AssetPath, - Asset, AssetIndex, AssetLoadError, AssetServer, AssetServerMode, Assets, ErasedAssetIndex, + Asset, AssetEntity, AssetEntityDoesNotExistError, AssetLoadError, AssetServer, AssetServerMode, Handle, UntypedAssetId, UntypedHandle, }; use alloc::{boxed::Box, string::ToString, vec::Vec}; @@ -143,7 +144,7 @@ pub(crate) struct LabeledAsset { /// * Loader dependencies: dependencies whose actual asset values are used during the load process pub struct LoadedAsset { pub(crate) value: A, - pub(crate) dependencies: HashSet, + pub(crate) dependencies: HashSet, pub(crate) loader_dependencies: HashMap, AssetHash>, /// The subassets of this asset. pub(crate) labeled_assets: Vec, @@ -160,11 +161,8 @@ impl LoadedAsset { /// Create a new loaded asset. This will use [`VisitAssetDependencies`](crate::VisitAssetDependencies) to populate `dependencies`. pub fn new_with_dependencies(value: A) -> Self { let mut dependencies = >::default(); - value.visit_dependencies(&mut |id| { - let Ok(asset_index) = id.try_into() else { - return; - }; - dependencies.insert(asset_index); + value.visit_dependencies(&mut |entity| { + dependencies.insert(entity); }); LoadedAsset { value, @@ -219,7 +217,7 @@ impl From for LoadedAsset { /// A "type erased / boxed" counterpart to [`LoadedAsset`]. This is used in places where the loaded type is not statically known. pub struct ErasedLoadedAsset { pub(crate) value: Box, - pub(crate) dependencies: HashSet, + pub(crate) dependencies: HashSet, pub(crate) loader_dependencies: HashMap, AssetHash>, /// The subassets of this asset. pub(crate) labeled_assets: Vec, @@ -317,21 +315,26 @@ impl ErasedLoadedAsset { } } -/// A type erased container for an [`Asset`] value that is capable of inserting the [`Asset`] into a [`World`]'s [`Assets`] collection. +/// A type erased container for an [`Asset`] value that is capable of inserting the [`Asset`] into +/// an entity in a [`World`]. pub(crate) trait AssetContainer: Downcast + Any + Send + Sync + 'static { - fn insert(self: Box, id: AssetIndex, world: &mut World); + fn insert( + self: Box, + entity: AssetEntity, + world: &mut World, + ) -> Result<(), AssetEntityDoesNotExistError>; fn asset_type_name(&self) -> &'static str; } impl_downcast!(AssetContainer); impl AssetContainer for A { - fn insert(self: Box, index: AssetIndex, world: &mut World) { - // We only ever call this if we know the asset is still alive, so it is fine to unwrap here. - world - .resource_mut::>() - .insert(index, *self) - .expect("the AssetIndex is still valid"); + fn insert( + self: Box, + entity: AssetEntity, + world: &mut World, + ) -> Result<(), AssetEntityDoesNotExistError> { + insert_asset(world, entity, *self) } fn asset_type_name(&self) -> &'static str { @@ -383,7 +386,7 @@ pub struct LoadContext<'a> { pub(crate) should_load_dependencies: bool, populate_hashes: bool, asset_path: AssetPath<'static>, - pub(crate) dependencies: HashSet, + pub(crate) dependencies: HashSet, /// Direct dependencies used by this loader. pub(crate) loader_dependencies: HashMap, AssetHash>, /// Stores the subassets added to this context. @@ -550,14 +553,8 @@ impl<'a> LoadContext<'a> { // `LoadedAsset`, `ErasedLoadedAsset`, or `LoadContext` (for mutating existing subassets), // we should move this to some point after those mutations are not possible. This spot is // convenient because we still have access to the static type of `A`. - value.visit_dependencies(&mut |asset_id| { - let (type_id, index) = match asset_id { - UntypedAssetId::Index { type_id, index } => (type_id, index), - // UUID assets can't be loaded anyway, so just ignore this ID. - UntypedAssetId::Uuid { .. } => return, - }; - self.dependencies - .insert(ErasedAssetIndex { index, type_id }); + value.visit_dependencies(&mut |entity| { + self.dependencies.insert(entity); }); LoadedAsset { value, @@ -626,8 +623,8 @@ impl<'a> LoadContext<'a> { let path = self.asset_path.clone().with_label(label); let handle = self.asset_server.get_or_create_path_handle(path, None); // `get_or_create_path_handle` always returns a Strong variant, so we are safe to unwrap. - let index = (&handle).try_into().unwrap(); - self.dependencies.insert(index); + let entity = handle.entity().unwrap(); + self.dependencies.insert(entity); handle } diff --git a/crates/bevy_asset/src/loader_builders.rs b/crates/bevy_asset/src/loader_builders.rs index 70a4f18b8192c..19b8199594c3a 100644 --- a/crates/bevy_asset/src/loader_builders.rs +++ b/crates/bevy_asset/src/loader_builders.rs @@ -8,7 +8,7 @@ use crate::{ LoadDirectError, LoadedAsset, LoadedUntypedAsset, UntypedHandle, }; use alloc::{borrow::ToOwned, boxed::Box, sync::Arc}; -use core::any::{type_name, TypeId}; +use core::any::TypeId; use std::path::Path; use tracing::error; @@ -92,7 +92,7 @@ impl<'ctx, 'builder> NestedLoadBuilder<'ctx, 'builder> { // The doc comment slightly lies: if `LoadContext::should_load_dependencies` is true, the // load will not be started, but the matching handle will still be returned. The caller // can't tell the difference. - self.load_internal(TypeId::of::(), Some(type_name::()), path.into()) + self.load_internal(TypeId::of::(), path.into()) .typed_debug_checked() } @@ -101,7 +101,7 @@ impl<'ctx, 'builder> NestedLoadBuilder<'ctx, 'builder> { /// This is a "deferred" load, meaning the caller will not have access to the loaded data; to /// access the loaded data, use [`Self::load_erased_value`]. pub fn load_erased<'a>(self, type_id: TypeId, path: impl Into>) -> UntypedHandle { - self.load_internal(type_id, None, path.into()) + self.load_internal(type_id, path.into()) } /// Loads the provided path with an unknown type (which is guessed based on the path or meta @@ -131,7 +131,7 @@ impl<'ctx, 'builder> NestedLoadBuilder<'ctx, 'builder> { }; // `load_unknown_type_with_meta_transform` and `get_or_create_path_handle` always returns a // Strong variant, so we are safe to unwrap. - let index = (&handle).try_into().unwrap(); + let index = handle.entity().unwrap(); self.load_context.dependencies.insert(index); handle } @@ -221,12 +221,7 @@ impl<'ctx, 'builder> NestedLoadBuilder<'ctx, 'builder> { /// Acquires the handle for the given type and path, and if necessary, begins a corresponding /// (deferred) load. - fn load_internal<'a>( - self, - type_id: TypeId, - type_name: Option<&str>, - path: AssetPath<'a>, - ) -> UntypedHandle { + fn load_internal<'a>(self, type_id: TypeId, path: AssetPath<'a>) -> UntypedHandle { let path = path.to_owned(); if path.path() == Path::new("") { error!("Attempted to load an asset with an empty path \"{path}\"!"); @@ -236,7 +231,6 @@ impl<'ctx, 'builder> NestedLoadBuilder<'ctx, 'builder> { self.load_context.asset_server.load_with_meta_transform( path, type_id, - type_name, self.meta_transform, (), self.override_unapproved, @@ -244,11 +238,11 @@ impl<'ctx, 'builder> NestedLoadBuilder<'ctx, 'builder> { } else { self.load_context .asset_server - .get_or_create_path_handle_erased(path, type_id, type_name, self.meta_transform) + .get_or_create_path_handle_erased(path, type_id, self.meta_transform) }; // `load_with_meta_transform` and `get_or_create_path_handle` always returns a Strong // variant, so we are safe to unwrap. - let index = (&handle).try_into().unwrap(); + let index = handle.entity().unwrap(); self.load_context.dependencies.insert(index); handle } diff --git a/crates/bevy_asset/src/meta.rs b/crates/bevy_asset/src/meta.rs index b031a74d76418..756d8e6c7cb61 100644 --- a/crates/bevy_asset/src/meta.rs +++ b/crates/bevy_asset/src/meta.rs @@ -17,7 +17,7 @@ use crate::{ io::{AssetReaderError, Reader}, loader::AssetLoader, processor::Process, - Asset, AssetPath, DeserializeMetaError, VisitAssetDependencies, + Asset, AssetEntity, AssetPath, DeserializeMetaError, VisitAssetDependencies, }; use downcast_rs::{impl_downcast, Downcast}; use ron::ser::PrettyConfig; @@ -232,7 +232,7 @@ impl Process for () { impl Asset for () {} impl VisitAssetDependencies for () { - fn visit_dependencies(&self, _visit: &mut impl FnMut(bevy_asset::UntypedAssetId)) { + fn visit_dependencies(&self, _visit: &mut impl FnMut(AssetEntity)) { unreachable!() } } diff --git a/crates/bevy_asset/src/processor/mod.rs b/crates/bevy_asset/src/processor/mod.rs index 1254528a43b19..44434058df809 100644 --- a/crates/bevy_asset/src/processor/mod.rs +++ b/crates/bevy_asset/src/processor/mod.rs @@ -53,8 +53,9 @@ use crate::{ get_asset_hash, get_full_asset_hash, AssetAction, AssetActionMinimal, AssetHash, AssetMeta, AssetMetaDyn, AssetMetaMinimal, ProcessedInfo, ProcessedInfoMinimal, }, - AssetLoadError, AssetMetaCheck, AssetPath, AssetServer, AssetServerMode, DeserializeMetaError, - MissingAssetLoaderForExtensionError, UnapprovedPathMode, WriteDefaultMetaError, + AssetHandleProvider, AssetLoadError, AssetMetaCheck, AssetPath, AssetServer, AssetServerMode, + AssetUuidMap, DeserializeMetaError, MissingAssetLoaderForExtensionError, UnapprovedPathMode, + WriteDefaultMetaError, }; use alloc::{borrow::ToOwned, boxed::Box, string::String, sync::Arc, vec, vec::Vec}; use bevy_ecs::prelude::*; @@ -166,9 +167,17 @@ impl AssetProcessor { sources.gate_on_processor(state.clone()); let sources = Arc::new(sources); + // Create a fake handle provider and remote allocator. We never actually need these handles + // to behave properly - we just need the handles to store inside of assets (to later save). + let remote_allocator = World::new().entity_allocator_mut().build_remote_allocator(); + let handle_provider = AssetHandleProvider::fake(); + let data = Arc::new(AssetProcessorData::new(sources.clone(), state)); // The asset processor uses its own asset server with its own id space let server = AssetServer::new_with_meta_check( + remote_allocator, + handle_provider, + AssetUuidMap::default(), sources.clone(), AssetServerMode::Processed, AssetMetaCheck::Always, diff --git a/crates/bevy_asset/src/reflect.rs b/crates/bevy_asset/src/reflect.rs index 792aaddb6a2da..2654be7ed7a0a 100644 --- a/crates/bevy_asset/src/reflect.rs +++ b/crates/bevy_asset/src/reflect.rs @@ -1,42 +1,53 @@ use alloc::{borrow::Cow, boxed::Box, format}; -use core::any::{Any, TypeId}; +use bevy_platform::collections::hash_map::Keys; +use core::{ + any::{Any, TypeId}, + iter::empty, +}; use serde::{de::Error as _, ser::Error as _, Deserialize, Deserializer, Serialize}; use thiserror::Error; use tracing::warn; use uuid::Uuid; -use bevy_ecs::world::{unsafe_world_cell::UnsafeWorldCell, World}; +use bevy_ecs::{ + archetype::{ArchetypeEntity, ArchetypeId, ArchetypeRecord}, + world::{unsafe_world_cell::UnsafeWorldCell, World}, +}; use bevy_reflect::{ serde::{ReflectDeserializerProcessor, ReflectSerializerProcessor}, CreateTypeData, FromReflect, PartialReflect, Reflect, TypeRegistry, }; use crate::{ - Asset, AssetId, AssetPath, AssetServer, Assets, Handle, InvalidGenerationError, LoadContext, - UntypedAssetId, UntypedHandle, + Asset, AssetData, AssetEntity, AssetId, AssetPath, AssetServer, AssetUuidMap, + DirectAssetAccessExt, Handle, InsertAssetError, LoadContext, UntypedAssetId, UntypedHandle, }; /// Type data for the [`TypeRegistry`] used to operate on reflected [`Asset`]s. /// -/// This type provides similar methods to [`Assets`] like [`get`](ReflectAsset::get), -/// [`add`](ReflectAsset::add) and [`remove`](ReflectAsset::remove), but can be used in situations where you don't know which asset type `T` you want -/// until runtime. +/// This type provides similar methods to [`Assets`](crate::Assets), +/// [`AssetsMut`](crate::AssetsMut), and [`AssetCommands`](crate::AssetCommands), like +/// [`get`](ReflectAsset::get), +/// [`spawn`](ReflectAsset::spawn), and [`remove`](ReflectAsset::remove), but can be used in +/// situations where you don't know which asset type `T` you want until runtime. /// -/// [`ReflectAsset`] can be obtained via [`TypeRegistration::data`](bevy_reflect::TypeRegistration::data) if the asset was registered using [`register_asset_reflect`](crate::AssetApp::register_asset_reflect). +/// [`ReflectAsset`] can be obtained via +/// [`TypeRegistration::data`](bevy_reflect::TypeRegistration::data) if the asset was registered +/// using [`register_asset_reflect`](crate::AssetApp::register_asset_reflect). #[derive(Clone)] pub struct ReflectAsset { handle_type_id: TypeId, - assets_resource_type_id: TypeId, + asset_data_type_id: TypeId, get: fn(&World, UntypedAssetId) -> Option<&dyn Reflect>, // SAFETY: - // - may only be called with an [`UnsafeWorldCell`] which can be used to access the corresponding `Assets` resource mutably + // - may only be called with an [`UnsafeWorldCell`] which can be used to read `AssetUuidMap` + // resource, and the `AssetData` component mutably // - may only be used to access **at most one** access at once get_unchecked_mut: unsafe fn(UnsafeWorldCell<'_>, UntypedAssetId) -> Option<&mut dyn Reflect>, - add: fn(&mut World, &dyn PartialReflect) -> UntypedHandle, - insert: - fn(&mut World, UntypedAssetId, &dyn PartialReflect) -> Result<(), InvalidGenerationError>, - len: fn(&World) -> usize, + spawn: fn(&mut World, &dyn PartialReflect) -> UntypedHandle, + insert: fn(&mut World, UntypedAssetId, &dyn PartialReflect) -> Result<(), InsertAssetError>, + count: fn(&World) -> usize, ids: for<'w> fn(&'w World) -> Box + 'w>, remove: fn(&mut World, UntypedAssetId) -> Option>, } @@ -47,12 +58,12 @@ impl ReflectAsset { self.handle_type_id } - /// The [`TypeId`] of the [`Assets`] resource - pub fn assets_resource_type_id(&self) -> TypeId { - self.assets_resource_type_id + /// The [`TypeId`] of the [`AssetData`] component. + pub fn asset_data_type_id(&self) -> TypeId { + self.asset_data_type_id } - /// Equivalent of [`Assets::get`] + /// Equivalent of [`Assets::get`](crate::Assets::get) pub fn get<'w>( &self, world: &'w World, @@ -61,7 +72,7 @@ impl ReflectAsset { (self.get)(world, asset_id.into()) } - /// Equivalent of [`Assets::get_mut`] + /// Equivalent of [`AssetsMut::get_mut`](crate::AssetsMut::get) pub fn get_mut<'w>( &self, world: &'w mut World, @@ -71,18 +82,19 @@ impl ReflectAsset { unsafe_code, reason = "Use of unsafe `Self::get_unchecked_mut()` function." )] - // SAFETY: unique world access + // SAFETY: We have exclusive access to the whole world, which includes all unsafe { (self.get_unchecked_mut)(world.as_unsafe_world_cell(), asset_id.into()) } } - /// Equivalent of [`Assets::get_mut`], but works with an [`UnsafeWorldCell`]. + /// Equivalent of [`AssetsMut::get_mut`](crate::AssetsMut::get_mut), but works with an + /// [`UnsafeWorldCell`]. /// - /// Only use this method when you have ensured that you are the *only* one with access to the [`Assets`] resource of the asset type. - /// Furthermore, this does *not* allow you to have look up two distinct handles, - /// you can only have at most one alive at the same time. - /// This means that this is *not allowed*: + /// Only use this method when you have ensured that you are the *only* one with access to the + /// [`AssetData`] of the asset type. Furthermore, this does *not* allow you to have look up two + /// distinct handles, you can only have at most one alive at the same time. This means that this + /// is *not allowed*: /// ```no_run /// # use bevy_asset::{ReflectAsset, UntypedHandle}; /// # use bevy_ecs::prelude::World; @@ -102,7 +114,8 @@ impl ReflectAsset { /// # Safety /// This method does not prevent you from having two mutable pointers to the same data, /// violating Rust's aliasing rules. To avoid this: - /// * Only call this method if you know that the [`UnsafeWorldCell`] may be used to access the corresponding `Assets` + /// * Only call this method if you know that the [`UnsafeWorldCell`] may be used to access the + /// corresponding [`AssetData`]. /// * Don't call this method more than once in the same scope. #[expect( unsafe_code, @@ -117,21 +130,21 @@ impl ReflectAsset { unsafe { (self.get_unchecked_mut)(world, asset_id.into()) } } - /// Equivalent of [`Assets::add`] - pub fn add(&self, world: &mut World, value: &dyn PartialReflect) -> UntypedHandle { - (self.add)(world, value) + /// Equivalent of [`DirectAssetAccessExt::spawn_asset`] + pub fn spawn(&self, world: &mut World, value: &dyn PartialReflect) -> UntypedHandle { + (self.spawn)(world, value) } - /// Equivalent of [`Assets::insert`] + /// Equivalent of [`DirectAssetAccessExt::insert_asset`] pub fn insert( &self, world: &mut World, asset_id: impl Into, value: &dyn PartialReflect, - ) -> Result<(), InvalidGenerationError> { + ) -> Result<(), InsertAssetError> { (self.insert)(world, asset_id.into(), value) } - /// Equivalent of [`Assets::remove`] + /// Equivalent of [`DirectAssetAccessExt::remove_asset`] pub fn remove( &self, world: &mut World, @@ -140,17 +153,17 @@ impl ReflectAsset { (self.remove)(world, asset_id.into()) } - /// Equivalent of [`Assets::len`] - pub fn len(&self, world: &World) -> usize { - (self.len)(world) + /// Equivalent of [`Assets::count`](crate::Assets::count) + pub fn count(&self, world: &World) -> usize { + (self.count)(world) } - /// Equivalent of [`Assets::is_empty`] + /// Equivalent of [`Assets::is_empty`](crate::Assets::is_empty) pub fn is_empty(&self, world: &World) -> bool { - self.len(world) == 0 + self.count(world) == 0 } - /// Equivalent of [`Assets::ids`] + /// Similar to [`Assets::iter`](crate::Assets::iter). pub fn ids<'w>(&self, world: &'w World) -> impl Iterator + 'w { (self.ids)(world) } @@ -160,44 +173,114 @@ impl CreateTypeData for ReflectAsset { fn create_type_data(_input: ()) -> Self { ReflectAsset { handle_type_id: TypeId::of::>(), - assets_resource_type_id: TypeId::of::>(), + asset_data_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 + .get_asset::(asset_id.typed_debug_checked()) + .map(|asset| asset as &dyn Reflect) }, 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.into_inner() as &mut dyn Reflect) + #[expect( + unsafe_code, + reason = "We are providing an abstraction over UnsafeWorldCell methods" + )] + // SAFETY: Caller ensures we have access to `AssetUuidMap`. + let entity = unsafe { world.get_resource::() } + .unwrap() + .resolve_entity(asset_id) + .ok()?; + let entity = world.get_entity(entity.raw_entity()).ok()?; + #[expect( + unsafe_code, + reason = "We are providing an abstraction over UnsafeWorldCell methods" + )] + // SAFETY: Caller ensures we have access to the asset data on this entity, and + // ensures we only have at most one reference to this asset. + let data = unsafe { entity.get_mut::>() }?.into_inner(); + Some(&mut data.0 as _) }, - add: |world, value| { - let mut assets = world.resource_mut::>(); + spawn: |world, value| { let value: A = FromReflect::from_reflect(value) .expect("could not call `FromReflect::from_reflect` in `ReflectAsset::add`"); - assets.add(value).untyped() + world.spawn_asset(value).untyped() }, insert: |world, asset_id, value| { - let mut assets = world.resource_mut::>(); let value: A = FromReflect::from_reflect(value) .expect("could not call `FromReflect::from_reflect` in `ReflectAsset::set`"); - assets.insert(asset_id.typed_debug_checked(), value) + world.insert_asset(asset_id.typed_debug_checked(), value) }, - len: |world| { - let assets = world.resource::>(); - assets.len() + count: |world| { + let Some(component_id) = world.components().get_id(TypeId::of::>()) + else { + return 0; + }; + let Some(archetypes) = world.archetypes().component_index().get(&component_id) + else { + return 0; + }; + archetypes + .keys() + .map(|id| world.archetypes().get(*id).unwrap()) + .map(|archetype| archetype.entities().len()) + .sum() }, ids: |world| { - let assets = world.resource::>(); - Box::new(assets.ids().map(AssetId::untyped)) + let Some(component_id) = world.components().get_id(TypeId::of::>()) + else { + return Box::new(empty::()); + }; + let Some(archetypes) = world.archetypes().component_index().get(&component_id) + else { + return Box::new(empty::()); + }; + let mut archetype_ids = archetypes.keys(); + let Some(first_id) = archetype_ids.next() else { + return Box::new(empty::()); + }; + let archetype = world.archetypes().get(*first_id).unwrap(); + let entities = archetype.entities().iter(); + + struct AssetIdIter<'w> { + world: &'w World, + archetype_ids: Keys<'w, ArchetypeId, ArchetypeRecord>, + entities: core::slice::Iter<'w, ArchetypeEntity>, + type_id: TypeId, + } + + impl Iterator for AssetIdIter<'_> { + type Item = UntypedAssetId; + + fn next(&mut self) -> Option { + // Loop until we either get an entity, or we run out of archetypes. + loop { + if let Some(archetype_entity) = self.entities.next() { + return Some(UntypedAssetId::Entity { + type_id: self.type_id, + entity: AssetEntity::new_unchecked(archetype_entity.id()), + }); + } + + // We ran out of entities in this archetype, so move on to the next + // archetype. + let archetype_id = self.archetype_ids.next()?; + let archetype = self.world.archetypes().get(*archetype_id).unwrap(); + self.entities = archetype.entities().iter(); + } + } + } + + Box::new(AssetIdIter { + world, + archetype_ids, + entities, + type_id: TypeId::of::(), + }) }, 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) + world + .remove_asset::(asset_id.typed_debug_checked()) + .ok() + .map(|asset| Box::new(asset) as _) }, } } @@ -522,9 +605,8 @@ mod tests { use crate::{ tests::{create_app, run_app_until, CoolText, CoolTextLoader, CoolTextRon, SubText}, - Asset, AssetApp, AssetServer, Assets, DirectAssetAccessExt, EphemeralHandleBehavior, - Handle, HandleDeserializeProcessor, HandleSerializeProcessor, LoadedUntypedAsset, - ReflectAsset, UntypedHandle, + Asset, AssetApp, AssetServer, DirectAssetAccessExt, EphemeralHandleBehavior, Handle, + HandleDeserializeProcessor, HandleSerializeProcessor, ReflectAsset, UntypedHandle, }; use bevy_ecs::reflect::AppTypeRegistry; use bevy_reflect::{ @@ -557,7 +639,7 @@ mod tests { field: "test".into(), }; - let handle = reflect_asset.add(app.world_mut(), &value); + let handle = reflect_asset.spawn(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) @@ -570,7 +652,7 @@ mod tests { .unwrap() .apply(&String::from("edited")); - assert_eq!(reflect_asset.len(app.world()), 1); + assert_eq!(reflect_asset.count(app.world()), 1); let ids: Vec<_> = reflect_asset.ids(app.world()).collect(); assert_eq!(ids.len(), 1); let id = ids[0]; @@ -579,7 +661,7 @@ mod tests { assert_eq!(asset.downcast_ref::().unwrap().field, "edited"); reflect_asset.remove(app.world_mut(), id).unwrap(); - assert_eq!(reflect_asset.len(app.world()), 0); + assert_eq!(reflect_asset.count(app.world()), 0); } fn serialize_as_cool_text(text: &str) -> String { @@ -627,15 +709,9 @@ mod tests { let untyped = asset_server.load_builder().load_untyped("def.cool.ron"); run_app_until(&mut app, |_| asset_server.is_loaded(&untyped).then_some(())); - let untyped = app - .world() - .resource::>() - .get(&untyped) - .unwrap() - .handle - .clone(); + let untyped = app.world().get_asset(untyped.id()).unwrap().handle.clone(); - let ephemeral = app.world_mut().add_asset(OtherAsset); + let ephemeral = app.world_mut().spawn_asset(OtherAsset); let stuff = Stuff { typed: asset_server.load("abc.cool.ron"), @@ -702,17 +778,12 @@ mod tests { // Make sure that the handles actually do end up with the correct assets. assert_eq!( - app.world() - .resource::>() - .get(&stuff.typed) - .unwrap() - .text, + app.world().get_asset(stuff.typed.id()).unwrap().text, "hello" ); assert_eq!( app.world() - .resource::>() - .get(&stuff.untyped.try_typed::().unwrap()) + .get_asset(stuff.untyped.try_typed::().unwrap().id()) .unwrap() .text, "world" diff --git a/crates/bevy_asset/src/render_asset.rs b/crates/bevy_asset/src/render_asset.rs index 90855e86e3389..dfaec6f2d6ef5 100644 --- a/crates/bevy_asset/src/render_asset.rs +++ b/crates/bevy_asset/src/render_asset.rs @@ -10,7 +10,7 @@ bitflags::bitflags! { /// /// Unloading the asset data saves on memory, as for most cases it is no longer necessary to keep /// it in RAM once it's been uploaded to the GPU's VRAM. However, this means you cannot access the - /// asset data from the CPU (via the `Assets` resource) once unloaded (without re-loading it). + /// asset data from the CPU (via the [`AssetData`](crate::AssetData) component) once unloaded (without re-loading it). /// /// If you never need access to the asset from the CPU past the first frame it's loaded on, /// or only need very infrequent access, then set this to `RENDER_WORLD`. Otherwise, set this to diff --git a/crates/bevy_asset/src/saver.rs b/crates/bevy_asset/src/saver.rs index 5061a88ee03b8..9dafc5a4e76b3 100644 --- a/crates/bevy_asset/src/saver.rs +++ b/crates/bevy_asset/src/saver.rs @@ -391,18 +391,14 @@ impl<'a> SavedAssetBuilder<'a> { asset: SavedAsset<'a, 'a, A>, ) -> Handle { let label = label.into(); - let handle = Handle::Strong( - self.asset_server - .read_infos() - .handle_providers - .get(&TypeId::of::()) - .expect("asset type has been initialized") - .reserve_handle_internal( - false, - Some(self.asset_path.clone().with_label(label.to_string())), - None, - ), - ); + let handle = self + .asset_server + .create_fake_handle( + TypeId::of::(), + Some(self.asset_path.clone().with_label(label.to_string())), + None, + ) + .typed_unchecked(); self.add_labeled_asset_with_existing_handle(label, asset, handle.clone()); handle } @@ -438,17 +434,10 @@ impl<'a> SavedAssetBuilder<'a> { asset: ErasedSavedAsset<'a, 'a>, ) -> UntypedHandle { let label = label.into(); - let handle = UntypedHandle::Strong( - self.asset_server - .read_infos() - .handle_providers - .get(&asset.value.type_id()) - .expect("asset type has been initialized") - .reserve_handle_internal( - false, - Some(self.asset_path.clone().with_label(label.to_string())), - None, - ), + let handle = self.asset_server.create_fake_handle( + asset.value.type_id(), + Some(self.asset_path.clone().with_label(label.to_string())), + None, ); self.add_labeled_asset_with_existing_handle_erased(label, asset, handle.clone()); handle @@ -588,7 +577,7 @@ pub(crate) mod tests { use crate::{ saver::{save_using_saver, AssetSaver, SavedAsset, SavedAssetBuilder}, tests::{create_app, run_app_until, CoolText, CoolTextLoader, CoolTextRon, SubText}, - AssetApp, AssetPath, AssetServer, Assets, + AssetApp, AssetPath, AssetServer, DirectAssetAccessExt, }; fn new_subtext(text: &str) -> SubText { @@ -696,22 +685,17 @@ pub(crate) mod tests { .unwrap(); } - let readback = asset_server.load("some/target/path.cool.ron"); + let readback = asset_server.load::("some/target/path.cool.ron"); run_app_until(&mut app, |_| { asset_server.is_loaded(&readback).then_some(()) }); - let cool_text = app - .world() - .resource::>() - .get(&readback) - .unwrap(); + let cool_text = app.world().get_asset(readback.id()).unwrap(); - let subtexts = app.world().resource::>(); let mut asset_labels = cool_text .sub_texts .iter() - .map(|handle| subtexts.get(handle).unwrap().text.clone()) + .map(|handle| app.world().get_asset(handle.id()).unwrap().text.clone()) .collect::>(); asset_labels.sort(); assert_eq!(asset_labels, &["goodbye", "hiya", "idk"]); @@ -725,13 +709,11 @@ pub(crate) mod tests { .init_asset::() .register_asset_loader(CoolTextLoader); - let mut subtexts = app.world_mut().resource_mut::>(); - let hiya_handle = subtexts.add(new_subtext("hiya")); - let goodbye_handle = subtexts.add(new_subtext("goodbye")); - let idk_handle = subtexts.add(new_subtext("idk")); + let hiya_handle = app.world_mut().spawn_asset(new_subtext("hiya")); + let goodbye_handle = app.world_mut().spawn_asset(new_subtext("goodbye")); + let idk_handle = app.world_mut().spawn_asset(new_subtext("idk")); - let mut cool_texts = app.world_mut().resource_mut::>(); - let cool_text_handle = cool_texts.add(CoolText { + let cool_text_handle = app.world_mut().spawn_asset(CoolText { text: "wassup".into(), sub_texts: vec![ hiya_handle.clone(), @@ -741,28 +723,27 @@ pub(crate) mod tests { ..Default::default() }); - let subtexts = app.world().resource::>(); - let cool_texts = app.world().resource::>(); let asset_server = app.world().resource::().clone(); let mut saved_asset_builder = SavedAssetBuilder::new(asset_server.clone(), "some/target/path.cool.ron".into()); saved_asset_builder.add_labeled_asset_with_existing_handle( "hiya", - SavedAsset::from_asset(subtexts.get(&hiya_handle).unwrap()), + SavedAsset::from_asset(app.world().get_asset(hiya_handle.id()).unwrap()), hiya_handle, ); saved_asset_builder.add_labeled_asset_with_existing_handle( "goodbye", - SavedAsset::from_asset(subtexts.get(&goodbye_handle).unwrap()), + SavedAsset::from_asset(app.world().get_asset(goodbye_handle.id()).unwrap()), goodbye_handle, ); saved_asset_builder.add_labeled_asset_with_existing_handle( "idk", - SavedAsset::from_asset(subtexts.get(&idk_handle).unwrap()), + SavedAsset::from_asset(app.world().get_asset(idk_handle.id()).unwrap()), idk_handle, ); - let saved_asset = saved_asset_builder.build(cool_texts.get(&cool_text_handle).unwrap()); + let saved_asset = + saved_asset_builder.build(app.world().get_asset(cool_text_handle.id()).unwrap()); let mut asset_labels = saved_asset .label_to_asset_index .keys() @@ -773,7 +754,7 @@ pub(crate) mod tests { // While this example is supported, it is **not** recommended. This currently blocks the // entire world from updating. A slow write could cause visible stutters. However we do this - // here to show it's possible to use assets directly out of the Assets resources. + // here to show it's possible to use assets directly out of the world. { let asset_server = asset_server.clone(); block_on(async move { @@ -789,22 +770,17 @@ pub(crate) mod tests { .unwrap(); } - let readback = asset_server.load("some/target/path.cool.ron"); + let readback = asset_server.load::("some/target/path.cool.ron"); run_app_until(&mut app, |_| { asset_server.is_loaded(&readback).then_some(()) }); - let cool_text = app - .world() - .resource::>() - .get(&readback) - .unwrap(); + let cool_text = app.world().get_asset(readback.id()).unwrap(); - let subtexts = app.world().resource::>(); let mut asset_labels = cool_text .sub_texts .iter() - .map(|handle| subtexts.get(handle).unwrap().text.clone()) + .map(|handle| app.world().get_asset(handle.id()).unwrap().text.clone()) .collect::>(); asset_labels.sort(); assert_eq!(asset_labels, &["goodbye", "hiya", "idk"]); diff --git a/crates/bevy_asset/src/server/info.rs b/crates/bevy_asset/src/server/info.rs index 2d7db08bd6a88..6eb33c5ca7775 100644 --- a/crates/bevy_asset/src/server/info.rs +++ b/crates/bevy_asset/src/server/info.rs @@ -1,8 +1,8 @@ use crate::{ meta::{AssetHash, MetaTransform}, - Asset, AssetHandleProvider, AssetIndex, AssetLoadError, AssetPath, DependencyLoadState, - ErasedAssetIndex, ErasedLoadedAsset, Handle, InternalAssetEvent, LoadState, - RecursiveDependencyLoadState, StrongHandle, UntypedHandle, + Asset, AssetEntity, AssetHandleProvider, AssetLoadError, AssetPath, DependencyLoadState, + ErasedLoadedAsset, Handle, InternalAssetEvent, LoadState, RecursiveDependencyLoadState, + StrongHandle, UntypedHandle, }; use alloc::{ borrow::ToOwned, @@ -10,31 +10,31 @@ use alloc::{ sync::{Arc, Weak}, vec::Vec, }; -use bevy_ecs::world::World; +use bevy_ecs::{entity::Entity, world::World}; use bevy_platform::collections::{hash_map::Entry, HashMap, HashSet}; use bevy_tasks::Task; use bevy_utils::{TypeIdMap, TypeIdMapEntry}; -use core::{ - any::{type_name, TypeId}, - task::Waker, -}; +use core::{any::TypeId, task::Waker}; use crossbeam_channel::Sender; use thiserror::Error; use tracing::warn; #[derive(Debug)] pub(crate) struct AssetInfo { + /// The ID of this asset. + type_id: TypeId, + /// A non-owning handle to the asset. weak_handle: Weak, pub(crate) path: Option>, pub(crate) load_state: LoadState, pub(crate) dep_load_state: DependencyLoadState, pub(crate) rec_dep_load_state: RecursiveDependencyLoadState, - loading_dependencies: HashSet, - failed_dependencies: HashSet, - loading_rec_dependencies: HashSet, - failed_rec_dependencies: HashSet, - dependents_waiting_on_load: HashSet, - dependents_waiting_on_recursive_dep_load: HashSet, + loading_dependencies: HashSet, + failed_dependencies: HashSet, + loading_rec_dependencies: HashSet, + failed_rec_dependencies: HashSet, + dependents_waiting_on_load: HashSet, + dependents_waiting_on_recursive_dep_load: HashSet, /// The asset paths required to load this asset. Hashes will only be set for processed assets. /// This is set using the value from [`LoadedAsset`]. /// This will only be populated if [`AssetInfos::watching_for_changes`] is set to `true` to @@ -42,16 +42,18 @@ pub(crate) struct AssetInfo { /// /// [`LoadedAsset`]: crate::loader::LoadedAsset loader_dependencies: HashMap, AssetHash>, - /// The number of handle drops to skip for this asset. - /// See usage (and comments) in `get_or_create_path_handle` for context. - handle_drops_to_skip: usize, /// List of tasks waiting for this asset to complete loading pub(crate) waiting_tasks: Vec, } impl AssetInfo { - fn new(weak_handle: Weak, path: Option>) -> Self { + fn new( + type_id: TypeId, + weak_handle: Weak, + path: Option>, + ) -> Self { Self { + type_id, weak_handle, path, load_state: LoadState::NotLoaded, @@ -64,7 +66,6 @@ impl AssetInfo { loader_dependencies: HashMap::default(), dependents_waiting_on_load: HashSet::default(), dependents_waiting_on_recursive_dep_load: HashSet::default(), - handle_drops_to_skip: 0, waiting_tasks: Vec::new(), } } @@ -77,10 +78,9 @@ pub(crate) struct AssetServerStats { pub(crate) started_load_tasks: usize, } -#[derive(Default)] pub(crate) struct AssetInfos { - path_to_index: HashMap, TypeIdMap>, - infos: HashMap, + path_to_entity: HashMap, TypeIdMap>, + infos: HashMap, /// If set to `true`, this informs [`AssetInfos`] to track data relevant to watching for changes (such as `load_dependents`) /// This should only be set at startup. pub(crate) watching_for_changes: bool, @@ -90,11 +90,11 @@ pub(crate) struct AssetInfos { /// Tracks living labeled assets for a given source asset. /// This should only be set when watching for changes to avoid unnecessary work. pub(crate) living_labeled_assets: HashMap, HashSet>>, - pub(crate) handle_providers: TypeIdMap, - pub(crate) dependency_loaded_event_sender: TypeIdMap, + pub(crate) handle_provider: AssetHandleProvider, + pub(crate) dependency_loaded_event_sender: TypeIdMap, pub(crate) dependency_failed_event_sender: - TypeIdMap, AssetLoadError)>, - pub(crate) pending_tasks: HashMap>, + TypeIdMap, AssetLoadError)>, + pub(crate) pending_tasks: HashMap>, /// The stats that have collected during usage of the asset server. pub(crate) stats: AssetServerStats, } @@ -102,49 +102,60 @@ pub(crate) struct AssetInfos { impl core::fmt::Debug for AssetInfos { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("AssetInfos") - .field("path_to_index", &self.path_to_index) + .field("path_to_index", &self.path_to_entity) .field("infos", &self.infos) .finish() } } impl AssetInfos { + pub(crate) fn new(handle_provider: AssetHandleProvider) -> Self { + Self { + handle_provider, + path_to_entity: Default::default(), + infos: Default::default(), + watching_for_changes: Default::default(), + loader_dependents: Default::default(), + living_labeled_assets: Default::default(), + dependency_loaded_event_sender: Default::default(), + dependency_failed_event_sender: Default::default(), + pending_tasks: Default::default(), + stats: Default::default(), + } + } + pub(crate) fn create_loading_handle_untyped( &mut self, type_id: TypeId, - type_name: &'static str, - ) -> UntypedHandle { - unwrap_with_context( - Self::create_handle_internal( - &mut self.infos, - &self.handle_providers, - &mut self.living_labeled_assets, - self.watching_for_changes, - type_id, - None, - None, - true, - ), + builder: &mut impl HandleBuilder, + ) -> Arc { + let entity = AssetEntity::new_unchecked(builder.create_entity()); + let handle = Self::create_handle_internal( + entity, + &mut self.infos, + &self.handle_provider, + &mut self.living_labeled_assets, + self.watching_for_changes, type_id, - Some(type_name), - ) - .unwrap() + None, + None, + true, + ); + builder.trigger_setup(&handle); + handle } fn create_handle_internal( - infos: &mut HashMap, - handle_providers: &TypeIdMap, + entity: AssetEntity, + infos: &mut HashMap, + handle_provider: &AssetHandleProvider, living_labeled_assets: &mut HashMap, HashSet>>, watching_for_changes: bool, type_id: TypeId, path: Option>, meta_transform: Option, loading: bool, - ) -> Result { - let provider = handle_providers - .get(&type_id) - .ok_or(MissingHandleProviderError(type_id))?; - + ) -> Arc { if watching_for_changes && let Some(path) = &path { let mut without_label = path.to_owned(); if let Some(label) = without_label.take_label() { @@ -153,16 +164,16 @@ impl AssetInfos { } } - let handle = provider.reserve_handle_internal(true, path.clone(), meta_transform); - let mut info = AssetInfo::new(Arc::downgrade(&handle), path); + let handle = handle_provider.create_handle(entity, type_id, path.clone(), meta_transform); + let mut info = AssetInfo::new(type_id, Arc::downgrade(&handle), path); if loading { info.load_state = LoadState::Loading; info.dep_load_state = DependencyLoadState::Loading; info.rec_dep_load_state = RecursiveDependencyLoadState::Loading; } - infos.insert(ErasedAssetIndex::new(handle.index, handle.type_id), info); + infos.insert(handle.entity, info); - Ok(UntypedHandle::Strong(handle)) + handle } pub(crate) fn get_or_create_path_handle( @@ -170,13 +181,14 @@ impl AssetInfos { path: AssetPath<'static>, loading_mode: HandleLoadingMode, meta_transform: Option, + builder: &mut impl HandleBuilder, ) -> (Handle, bool) { let (handle, should_load) = self.get_or_create_path_handle_erased( path, TypeId::of::(), - Some(type_name::()), loading_mode, meta_transform, + builder, ); (handle.typed_unchecked(), should_load) } @@ -185,18 +197,18 @@ impl AssetInfos { &mut self, path: AssetPath<'static>, type_id: TypeId, - type_name: Option<&str>, loading_mode: HandleLoadingMode, meta_transform: Option, + builder: &mut impl HandleBuilder, ) -> (UntypedHandle, bool) { - let result = self.get_or_create_path_handle_internal( + self.get_or_create_path_handle_internal( path, Some(type_id), loading_mode, meta_transform, - ); - // it is ok to unwrap because TypeId was specified above - unwrap_with_context(result, type_id, type_name).unwrap() + builder, + ) + .expect("type should be correct since the `TypeId` is specified above") } /// Retrieves asset tracking data, or creates it if it doesn't exist. @@ -207,8 +219,9 @@ impl AssetInfos { type_id: Option, loading_mode: HandleLoadingMode, meta_transform: Option, + builder: &mut impl HandleBuilder, ) -> Result<(UntypedHandle, bool), GetOrCreateHandleInternalError> { - let handles = self.path_to_index.entry(path.clone()).or_default(); + let handles = self.path_to_entity.entry(path.clone()).or_default(); let type_id = type_id .or_else(|| { @@ -221,85 +234,81 @@ impl AssetInfos { }) .ok_or(GetOrCreateHandleInternalError::HandleMissingButTypeIdNotSpecified)?; + // Note: Pass in `infos` here so we can borrow it later on. + let create_new_handle = |infos: &mut HashMap| { + let should_load = match loading_mode { + HandleLoadingMode::NotLoading => false, + HandleLoadingMode::Request | HandleLoadingMode::Force => true, + }; + let entity = AssetEntity::new_unchecked(builder.create_entity()); + let handle = AssetInfos::create_handle_internal( + entity, + infos, + &self.handle_provider, + &mut self.living_labeled_assets, + self.watching_for_changes, + type_id, + Some(path), + meta_transform, + should_load, + ); + builder.trigger_setup(&handle); + (handle, should_load, entity) + }; + match handles.entry(type_id) { - TypeIdMapEntry::Occupied(entry) => { - let index = *entry.get(); - // if there is a path_to_id entry, info always exists - let info = self - .infos - .get_mut(&ErasedAssetIndex::new(index, type_id)) - .unwrap(); - let mut should_load = false; - if loading_mode == HandleLoadingMode::Force - || (loading_mode == HandleLoadingMode::Request - && matches!(info.load_state, LoadState::NotLoaded | LoadState::Failed(_))) - { - info.load_state = LoadState::Loading; - info.dep_load_state = DependencyLoadState::Loading; - info.rec_dep_load_state = RecursiveDependencyLoadState::Loading; - should_load = true; - } + TypeIdMapEntry::Occupied(mut entry) => { + let entity = *entry.get(); + // if there is a path_to_entity entry, info always exists + let info = self.infos.get_mut(&entity).unwrap(); if let Some(strong_handle) = info.weak_handle.upgrade() { // If we can upgrade the handle, there is at least one live handle right now, // The asset load has already kicked off (and maybe completed), so we can just // return a strong handle + + let mut should_load = false; + if loading_mode == HandleLoadingMode::Force + || (loading_mode == HandleLoadingMode::Request + && matches!( + info.load_state, + LoadState::NotLoaded | LoadState::Failed(_) + )) + { + info.load_state = LoadState::Loading; + info.dep_load_state = DependencyLoadState::Loading; + info.rec_dep_load_state = RecursiveDependencyLoadState::Loading; + should_load = true; + } Ok((UntypedHandle::Strong(strong_handle), should_load)) } else { - // Asset meta exists, but all live handles were dropped. This means the `track_assets` system - // hasn't been run yet to remove the current asset - // (note that this is guaranteed to be transactional with the `track_assets` system - // because it locks the AssetInfos collection) - - // We must create a new strong handle for the existing id and ensure that the drop of the old - // strong handle doesn't remove the asset from the Assets collection - info.handle_drops_to_skip += 1; - let provider = self - .handle_providers - .get(&type_id) - .ok_or(MissingHandleProviderError(type_id))?; - let handle = provider.get_handle(index, true, Some(path), meta_transform); - info.weak_handle = Arc::downgrade(&handle); + // Asset meta exists, but all live handles were dropped. This means the + // `despawn_unused_assets` system hasn't been run yet to remove the current + // asset. We must create a new entity and handle for this path. + let (handle, should_load, entity) = create_new_handle(&mut self.infos); + entry.insert(entity); Ok((UntypedHandle::Strong(handle), should_load)) } } // The entry does not exist, so this is a "fresh" asset load. We must create a new handle TypeIdMapEntry::Vacant(entry) => { - let should_load = match loading_mode { - HandleLoadingMode::NotLoading => false, - HandleLoadingMode::Request | HandleLoadingMode::Force => true, - }; - let handle = Self::create_handle_internal( - &mut self.infos, - &self.handle_providers, - &mut self.living_labeled_assets, - self.watching_for_changes, - type_id, - Some(path), - meta_transform, - should_load, - )?; - let index = match &handle { - UntypedHandle::Strong(handle) => handle.index, - // `create_handle_internal` always returns Strong variant. - UntypedHandle::Uuid { .. } => unreachable!(), - }; - entry.insert(index); - Ok((handle, should_load)) + let (handle, should_load, entity) = create_new_handle(&mut self.infos); + entry.insert(entity); + Ok((UntypedHandle::Strong(handle), should_load)) } } } - pub(crate) fn get(&self, index: ErasedAssetIndex) -> Option<&AssetInfo> { - self.infos.get(&index) + pub(crate) fn get(&self, entity: AssetEntity) -> Option<&AssetInfo> { + self.infos.get(&entity) } - pub(crate) fn contains_key(&self, index: ErasedAssetIndex) -> bool { - self.infos.contains_key(&index) + pub(crate) fn contains_key(&self, entity: AssetEntity) -> bool { + self.infos.contains_key(&entity) } - pub(crate) fn get_mut(&mut self, index: ErasedAssetIndex) -> Option<&mut AssetInfo> { - self.infos.get_mut(&index) + pub(crate) fn get_mut(&mut self, entity: AssetEntity) -> Option<&mut AssetInfo> { + self.infos.get_mut(&entity) } pub(crate) fn get_path_and_type_id_handle( @@ -307,14 +316,14 @@ impl AssetInfos { path: &AssetPath<'_>, type_id: TypeId, ) -> Option { - let index = *self.path_to_index.get(path)?.get(&type_id)?; - self.get_index_handle(ErasedAssetIndex::new(index, type_id)) + let entity = *self.path_to_entity.get(path)?.get(&type_id)?; + self.get_index_handle(entity) } - pub(crate) fn get_path_indices<'a>( + pub(crate) fn get_path_entities<'a>( &'a self, path: &'a AssetPath<'_>, - ) -> impl Iterator + 'a { + ) -> impl Iterator + 'a { /// Concrete type to allow returning an `impl Iterator` even if `self.path_to_id.get(&path)` is `None` enum HandlesByPathIterator { None, @@ -323,9 +332,9 @@ impl AssetInfos { impl Iterator for HandlesByPathIterator where - T: Iterator, + T: Iterator, { - type Item = ErasedAssetIndex; + type Item = T::Item; fn next(&mut self) -> Option { match self { @@ -335,12 +344,8 @@ impl AssetInfos { } } - if let Some(type_id_to_id) = self.path_to_index.get(path) { - HandlesByPathIterator::Some( - type_id_to_id - .iter() - .map(|(type_id, index)| ErasedAssetIndex::new(*index, *type_id)), - ) + if let Some(type_id_to_entity) = self.path_to_entity.get(path) { + HandlesByPathIterator::Some(type_id_to_entity.values().cloned()) } else { HandlesByPathIterator::None } @@ -350,19 +355,19 @@ impl AssetInfos { &'a self, path: &'a AssetPath<'_>, ) -> impl Iterator + 'a { - self.get_path_indices(path) + self.get_path_entities(path) .filter_map(|id| self.get_index_handle(id)) } - pub(crate) fn get_index_handle(&self, index: ErasedAssetIndex) -> Option { - let info = self.infos.get(&index)?; + pub(crate) fn get_index_handle(&self, entity: AssetEntity) -> Option { + let info = self.infos.get(&entity)?; let strong_handle = info.weak_handle.upgrade()?; Some(UntypedHandle::Strong(strong_handle)) } /// Returns `true` if the asset this path points to is still alive pub(crate) fn is_path_alive<'a>(&self, path: impl Into>) -> bool { - self.get_path_indices(&path.into()) + self.get_path_entities(&path.into()) .filter_map(|id| self.infos.get(&id)) .any(|info| info.weak_handle.strong_count() > 0) } @@ -381,22 +386,22 @@ impl AssetInfos { } /// Returns `true` if the asset should be removed from the collection. - pub(crate) fn process_handle_drop(&mut self, index: ErasedAssetIndex) -> bool { + pub(crate) fn process_handle_drop(&mut self, entity: AssetEntity) { Self::process_handle_drop_internal( &mut self.infos, - &mut self.path_to_index, + &mut self.path_to_entity, &mut self.loader_dependents, &mut self.living_labeled_assets, &mut self.pending_tasks, self.watching_for_changes, - index, - ) + entity, + ); } /// Updates [`AssetInfo`] / load state for an asset that has finished loading (and relevant dependencies / dependents). pub(crate) fn process_asset_load( &mut self, - loaded_asset_index: ErasedAssetIndex, + entity: AssetEntity, loaded_asset: ErasedLoadedAsset, world: &mut World, sender: &Sender, @@ -407,23 +412,21 @@ impl AssetInfos { let UntypedHandle::Strong(handle) = &asset.handle else { unreachable!("Labeled assets are always strong handles"); }; - self.process_asset_load( - ErasedAssetIndex { - index: handle.index, - type_id: handle.type_id, - }, - asset.asset, - world, - sender, - ); + self.process_asset_load(handle.entity, asset.asset, world, sender); } // Check whether the handle has been dropped since the asset was loaded. - if !self.infos.contains_key(&loaded_asset_index) { + if !self.infos.contains_key(&entity) { return; } - loaded_asset.value.insert(loaded_asset_index.index, world); + let asset_type_id = loaded_asset.value.type_id(); + // This should be impossible, since we still have the asset server metadata, which means the + // metadata hasn't been removed by `AssetServerManaged`s hook. + loaded_asset + .value + .insert(entity, world) + .expect("asset metadata still exists in AssetServer"); let mut loading_deps = loaded_asset.dependencies; let mut failed_deps = >::default(); let mut dep_error = None; @@ -438,7 +441,7 @@ impl AssetInfos { // If dependency is loading, wait for it. dep_info .dependents_waiting_on_recursive_dep_load - .insert(loaded_asset_index); + .insert(entity); } RecursiveDependencyLoadState::Loaded => { // If dependency is loaded, reduce our count by one @@ -455,7 +458,7 @@ impl AssetInfos { match dep_info.load_state { LoadState::NotLoaded | LoadState::Loading => { // If dependency is loading, wait for it. - dep_info.dependents_waiting_on_load.insert(loaded_asset_index); + dep_info.dependents_waiting_on_load.insert(entity); true } LoadState::Loaded => { @@ -474,7 +477,7 @@ impl AssetInfos { // the dependency id does not exist, which implies it was manually removed or never existed in the first place warn!( "Dependency {} from asset {} is unknown. This asset's dependency load status will not switch to 'Loaded' until the unknown dependency is loaded.", - dep_id, loaded_asset_index + dep_id, entity ); true } @@ -490,7 +493,8 @@ impl AssetInfos { (0, 0) => { sender .send(InternalAssetEvent::LoadedWithDependencies { - index: loaded_asset_index, + entity, + type_id: asset_type_id, }) .unwrap(); RecursiveDependencyLoadState::Loaded @@ -505,7 +509,7 @@ impl AssetInfos { if watching_for_changes { let info = self .infos - .get(&loaded_asset_index) + .get(&entity) .expect("Asset info should always exist at this point"); if let Some(asset_path) = &info.path { for loader_dependency in loaded_asset.loader_dependencies.keys() { @@ -518,7 +522,7 @@ impl AssetInfos { } } let info = self - .get_mut(loaded_asset_index) + .get_mut(entity) .expect("Asset info should always exist at this point"); info.loading_dependencies = loading_deps; info.failed_dependencies = failed_deps; @@ -548,7 +552,7 @@ impl AssetInfos { for id in dependents_waiting_on_load { if let Some(info) = self.get_mut(id) { - info.loading_dependencies.remove(&loaded_asset_index); + info.loading_dependencies.remove(&entity); if info.loading_dependencies.is_empty() && !info.dep_load_state.is_failed() { // send dependencies loaded event info.dep_load_state = DependencyLoadState::Loaded; @@ -560,12 +564,12 @@ impl AssetInfos { match rec_dep_load_state { RecursiveDependencyLoadState::Loaded => { for dep_id in dependents_waiting_on_rec_load { - Self::propagate_loaded_state(self, loaded_asset_index, dep_id, sender); + Self::propagate_loaded_state(self, entity, dep_id, sender); } } RecursiveDependencyLoadState::Failed(ref error) => { for dep_id in dependents_waiting_on_rec_load { - Self::propagate_failed_state(self, loaded_asset_index, dep_id, error); + Self::propagate_failed_state(self, entity, dep_id, error); } } RecursiveDependencyLoadState::Loading | RecursiveDependencyLoadState::NotLoaded => { @@ -579,17 +583,20 @@ impl AssetInfos { /// Recursively propagates loaded state up the dependency tree. fn propagate_loaded_state( infos: &mut AssetInfos, - loaded_id: ErasedAssetIndex, - waiting_id: ErasedAssetIndex, + loaded_entity: AssetEntity, + waiting_entity: AssetEntity, sender: &Sender, ) { - let dependents_waiting_on_rec_load = if let Some(info) = infos.get_mut(waiting_id) { - info.loading_rec_dependencies.remove(&loaded_id); + let dependents_waiting_on_rec_load = if let Some(info) = infos.get_mut(waiting_entity) { + info.loading_rec_dependencies.remove(&loaded_entity); if info.loading_rec_dependencies.is_empty() && info.failed_rec_dependencies.is_empty() { info.rec_dep_load_state = RecursiveDependencyLoadState::Loaded; if info.load_state.is_loaded() { sender - .send(InternalAssetEvent::LoadedWithDependencies { index: waiting_id }) + .send(InternalAssetEvent::LoadedWithDependencies { + entity: waiting_entity, + type_id: info.type_id, + }) .unwrap(); } Some(core::mem::take( @@ -604,7 +611,7 @@ impl AssetInfos { if let Some(dependents_waiting_on_rec_load) = dependents_waiting_on_rec_load { for dep_id in dependents_waiting_on_rec_load { - Self::propagate_loaded_state(infos, waiting_id, dep_id, sender); + Self::propagate_loaded_state(infos, waiting_entity, dep_id, sender); } } } @@ -612,13 +619,13 @@ impl AssetInfos { /// Recursively propagates failed state up the dependency tree fn propagate_failed_state( infos: &mut AssetInfos, - failed_id: ErasedAssetIndex, - waiting_id: ErasedAssetIndex, + failed_entity: AssetEntity, + waiting_entity: AssetEntity, error: &Arc, ) { - let dependents_waiting_on_rec_load = if let Some(info) = infos.get_mut(waiting_id) { - info.loading_rec_dependencies.remove(&failed_id); - info.failed_rec_dependencies.insert(failed_id); + let dependents_waiting_on_rec_load = if let Some(info) = infos.get_mut(waiting_entity) { + info.loading_rec_dependencies.remove(&failed_entity); + info.failed_rec_dependencies.insert(failed_entity); info.rec_dep_load_state = RecursiveDependencyLoadState::Failed(error.clone()); Some(core::mem::take( &mut info.dependents_waiting_on_recursive_dep_load, @@ -629,24 +636,20 @@ impl AssetInfos { if let Some(dependents_waiting_on_rec_load) = dependents_waiting_on_rec_load { for dep_id in dependents_waiting_on_rec_load { - Self::propagate_failed_state(infos, waiting_id, dep_id, error); + Self::propagate_failed_state(infos, waiting_entity, dep_id, error); } } } - pub(crate) fn process_asset_fail( - &mut self, - failed_index: ErasedAssetIndex, - error: AssetLoadError, - ) { + pub(crate) fn process_asset_fail(&mut self, failed_entity: AssetEntity, error: AssetLoadError) { // Check whether the handle has been dropped since the asset was loaded. - if !self.infos.contains_key(&failed_index) { + if !self.infos.contains_key(&failed_entity) { return; } let error = Arc::new(error); let (dependents_waiting_on_load, dependents_waiting_on_rec_load) = { - let Some(info) = self.get_mut(failed_index) else { + let Some(info) = self.get_mut(failed_entity) else { // The asset was already dropped. return; }; @@ -662,10 +665,10 @@ impl AssetInfos { ) }; - for waiting_id in dependents_waiting_on_load { - if let Some(info) = self.get_mut(waiting_id) { - info.loading_dependencies.remove(&failed_index); - info.failed_dependencies.insert(failed_index); + for waiting_entity in dependents_waiting_on_load { + if let Some(info) = self.get_mut(waiting_entity) { + info.loading_dependencies.remove(&failed_entity); + info.failed_dependencies.insert(failed_entity); // don't overwrite DependencyLoadState if already failed to preserve first error if !info.dep_load_state.is_failed() { info.dep_load_state = DependencyLoadState::Failed(error.clone()); @@ -673,8 +676,8 @@ impl AssetInfos { } } - for waiting_id in dependents_waiting_on_rec_load { - Self::propagate_failed_state(self, failed_index, waiting_id, &error); + for waiting_entity in dependents_waiting_on_rec_load { + Self::propagate_failed_state(self, failed_entity, waiting_entity, &error); } } @@ -708,32 +711,25 @@ impl AssetInfos { } fn process_handle_drop_internal( - infos: &mut HashMap, - path_to_id: &mut HashMap, TypeIdMap>, + infos: &mut HashMap, + path_to_entity: &mut HashMap, TypeIdMap>, loader_dependents: &mut HashMap, HashSet>>, living_labeled_assets: &mut HashMap, HashSet>>, - pending_tasks: &mut HashMap>, + pending_tasks: &mut HashMap>, watching_for_changes: bool, - index: ErasedAssetIndex, - ) -> bool { - let Entry::Occupied(mut entry) = infos.entry(index) else { + entity: AssetEntity, + ) { + let Entry::Occupied(entry) = infos.entry(entity) else { // Either the asset was already dropped, it doesn't exist, or it isn't managed by the asset server // None of these cases should result in a removal from the Assets collection - return false; + return; }; - if entry.get_mut().handle_drops_to_skip > 0 { - entry.get_mut().handle_drops_to_skip -= 1; - return false; - } - - pending_tasks.remove(&index); - - let type_id = entry.key().type_id; + pending_tasks.remove(&entity); let info = entry.remove(); let Some(path) = &info.path else { - return true; + return; }; if watching_for_changes { @@ -745,41 +741,40 @@ impl AssetInfos { ); } - if let Some(map) = path_to_id.get_mut(path) { - map.shift_remove(&type_id); - + // Try to remove the entity from `path_to_entity`. + if let Some(map) = path_to_entity.get_mut(path) + && let TypeIdMapEntry::Occupied(entry) = map.entry(info.type_id) + // Make sure that `entity` is still the most "up-to-date" entity for this path. It may + // not be if this entity's handle was dropped, then another load occurred, and then that + // new entity was manually despawned. Very unlikely, but we don't need to do anything in + // that case. + && *entry.get() == entity + { + entry.shift_remove(); if map.is_empty() { - path_to_id.remove(path); + path_to_entity.remove(path); } }; - - true } /// Consumes all current handle drop events. This will update information in [`AssetInfos`], but it - /// will not affect [`Assets`] storages. For normal use cases, prefer `Assets::track_assets()` - /// This should only be called if `Assets` storage isn't being used (such as in [`AssetProcessor`](crate::processor::AssetProcessor)) - /// - /// [`Assets`]: crate::Assets + /// will not affect asset entities. For normal use cases, prefer [`despawn_unused_assets`](crate::despawn_unused_assets). + /// This should only be called if asset entities aren't being used (such as in [`AssetProcessor`](crate::processor::AssetProcessor)) pub(crate) fn consume_handle_drop_events(&mut self) { - for provider in self.handle_providers.values() { - while let Ok(drop_event) = provider.drop_receiver.try_recv() { - let id = drop_event.index; - if drop_event.asset_server_managed { - Self::process_handle_drop_internal( - &mut self.infos, - &mut self.path_to_index, - &mut self.loader_dependents, - &mut self.living_labeled_assets, - &mut self.pending_tasks, - self.watching_for_changes, - id, - ); - } - } + while let Ok((entity, _)) = self.handle_provider.drop_receiver.try_recv() { + Self::process_handle_drop_internal( + &mut self.infos, + &mut self.path_to_entity, + &mut self.loader_dependents, + &mut self.living_labeled_assets, + &mut self.pending_tasks, + self.watching_for_changes, + entity, + ); } } } + /// Determines how a handle should be initialized #[derive(Copy, Clone, PartialEq, Eq)] pub(crate) enum HandleLoadingMode { @@ -791,36 +786,18 @@ pub(crate) enum HandleLoadingMode { Force, } -#[derive(Error, Debug)] -#[error("Cannot allocate a handle because no handle provider exists for asset type {0:?}")] -pub struct MissingHandleProviderError(TypeId); +/// An (internal) trait for creating handles. +pub(crate) trait HandleBuilder { + /// Creates the entity that will be used for the handle. + fn create_entity(&mut self) -> Entity; + + /// Invokes the setup of the asset for the newly created handle. + fn trigger_setup(&mut self, handle: &Arc); +} /// An error encountered during [`AssetInfos::get_or_create_path_handle_internal`]. #[derive(Error, Debug)] pub(crate) enum GetOrCreateHandleInternalError { - #[error(transparent)] - MissingHandleProviderError(#[from] MissingHandleProviderError), #[error("Handle does not exist but TypeId was not specified.")] HandleMissingButTypeIdNotSpecified, } - -pub(crate) fn unwrap_with_context( - result: Result, - type_id: TypeId, - type_name: Option<&str>, -) -> Option { - match result { - Ok(value) => Some(value), - Err(GetOrCreateHandleInternalError::HandleMissingButTypeIdNotSpecified) => None, - Err(GetOrCreateHandleInternalError::MissingHandleProviderError(_)) => match type_name { - Some(type_name) => { - panic!("Cannot allocate an Asset Handle of type '{type_name}' because the asset type has not been initialized. \ - Make sure you have called `app.init_asset::<{type_name}>()`"); - } - None => { - panic!("Cannot allocate an AssetHandle of type '{type_id:?}' because the asset type has not been initialized. \ - Make sure you have called `app.init_asset::<(actual asset type)>()`") - } - }, - } -} diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index 0eaa9f68d76c0..a865dbf0b7a3c 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -14,31 +14,26 @@ use crate::{ MetaTransform, Settings, }, path::AssetPath, - Asset, AssetEvent, AssetHandleProvider, AssetId, AssetIndex, AssetLoadFailedEvent, - AssetMetaCheck, Assets, DeserializeMetaError, ErasedAssetIndex, ErasedLoadedAsset, Handle, - LoadedUntypedAsset, UnapprovedPathMode, UntypedAssetId, UntypedAssetLoadFailedEvent, - UntypedHandle, VisitAssetDependencies, + setup_asset, Asset, AssetEntity, AssetEvent, AssetHandleProvider, AssetId, + AssetLoadFailedEvent, AssetMetaCheck, AssetUuidMap, DeserializeMetaError, ErasedLoadedAsset, + Handle, LoadedUntypedAsset, StrongHandle, UnapprovedPathMode, UntypedAssetId, + UntypedAssetLoadFailedEvent, UntypedHandle, VisitAssetDependencies, }; use alloc::{borrow::ToOwned, boxed::Box, vec, vec::Vec}; use alloc::{ format, string::{String, ToString}, - sync::Arc, + sync::{Arc, Weak}, }; use atomicow::CowArc; use bevy_diagnostic::{DiagnosticPath, Diagnostics}; -use bevy_ecs::prelude::*; +use bevy_ecs::{entity::RemoteAllocator, lifecycle::HookContext, prelude::*, world::DeferredWorld}; use bevy_platform::{ collections::HashSet, sync::{PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard}, }; use bevy_tasks::IoTaskPool; -use core::{ - any::{type_name, TypeId}, - future::Future, - panic::AssertUnwindSafe, - task::Poll, -}; +use core::{any::TypeId, future::Future, marker::PhantomData, panic::AssertUnwindSafe, task::Poll}; use crossbeam_channel::{Receiver, Sender}; use futures_lite::{FutureExt, StreamExt}; use info::*; @@ -52,7 +47,7 @@ use tracing::{error, info, warn}; /// /// The general process to load an asset is: /// 1. Initialize a new [`Asset`] type with the [`AssetServer`] via [`AssetApp::init_asset`], which -/// will internally call [`AssetServer::register_asset`] and set up related ECS [`Assets`] +/// will internally call [`AssetServer::register_asset`] and set up related ECS [`Asset`] /// storage and systems. /// 2. Register one or more [`AssetLoader`]s for that asset with [`AssetApp::init_asset_loader`] /// 3. Add the asset to your asset folder (defaults to `assets`). @@ -70,6 +65,8 @@ pub struct AssetServer { /// Internal data used by [`AssetServer`]. This is intended to be used from within an [`Arc`]. pub(crate) struct AssetServerData { pub(crate) infos: RwLock, + /// A mapping from asset UUIDs to their entity. + uuid_map: AssetUuidMap, pub(crate) loaders: Arc>, asset_event_sender: Sender, asset_event_receiver: Receiver, @@ -77,6 +74,7 @@ pub(crate) struct AssetServerData { mode: AssetServerMode, meta_check: AssetMetaCheck, unapproved_path_mode: UnapprovedPathMode, + remote_allocator: RemoteAllocator, } /// The "asset mode" the server is currently in. @@ -94,25 +92,10 @@ impl AssetServer { /// Create a new instance of [`AssetServer`]. If `watch_for_changes` is true, the [`AssetReader`](crate::io::AssetReader) storage will watch for changes to /// asset sources and hot-reload them. - pub fn new( - sources: Arc, - mode: AssetServerMode, - watching_for_changes: bool, - unapproved_path_mode: UnapprovedPathMode, - ) -> Self { - Self::new_with_loaders( - sources, - Default::default(), - mode, - AssetMetaCheck::Always, - watching_for_changes, - unapproved_path_mode, - ) - } - - /// Create a new instance of [`AssetServer`]. If `watch_for_changes` is true, the [`AssetReader`](crate::io::AssetReader) storage will watch for changes to - /// asset sources and hot-reload them. - pub fn new_with_meta_check( + pub(crate) fn new_with_meta_check( + remote_allocator: RemoteAllocator, + handle_provider: AssetHandleProvider, + uuid_map: AssetUuidMap, sources: Arc, mode: AssetServerMode, meta_check: AssetMetaCheck, @@ -120,6 +103,9 @@ impl AssetServer { unapproved_path_mode: UnapprovedPathMode, ) -> Self { Self::new_with_loaders( + remote_allocator, + handle_provider, + uuid_map, sources, Default::default(), mode, @@ -130,6 +116,9 @@ impl AssetServer { } pub(crate) fn new_with_loaders( + remote_allocator: RemoteAllocator, + handle_provider: AssetHandleProvider, + uuid_map: AssetUuidMap, sources: Arc, loaders: Arc>, mode: AssetServerMode, @@ -138,16 +127,18 @@ impl AssetServer { unapproved_path_mode: UnapprovedPathMode, ) -> Self { let (asset_event_sender, asset_event_receiver) = crossbeam_channel::unbounded(); - let mut infos = AssetInfos::default(); + let mut infos = AssetInfos::new(handle_provider); infos.watching_for_changes = watching_for_changes; Self { data: Arc::new(AssetServerData { + remote_allocator, sources, mode, meta_check, asset_event_sender, asset_event_receiver, loaders, + uuid_map, infos: RwLock::new(infos), unapproved_path_mode, }), @@ -201,23 +192,30 @@ impl AssetServer { } /// Registers a new [`Asset`] type. [`Asset`] types must be registered before assets of that type can be loaded. - pub fn register_asset(&self, assets: &Assets) { - self.register_handle_provider(assets.get_handle_provider()); - fn sender(world: &mut World, index: AssetIndex) { - world - .resource_mut::>>() - .write(AssetEvent::LoadedWithDependencies { id: index.into() }); + pub fn register_asset(&self) { + fn sender(world: &mut World, entity: AssetEntity) { + world.resource_mut::>>().write( + AssetEvent::::LoadedWithDependencies { + id: AssetId::Entity { + entity, + marker: PhantomData, + }, + }, + ); } fn failed_sender( world: &mut World, - index: AssetIndex, + entity: AssetEntity, path: AssetPath<'static>, error: AssetLoadError, ) { world .resource_mut::>>() - .write(AssetLoadFailedEvent { - id: index.into(), + .write(AssetLoadFailedEvent:: { + id: AssetId::Entity { + entity, + marker: PhantomData, + }, path, error, }); @@ -234,12 +232,6 @@ impl AssetServer { .insert(TypeId::of::(), failed_sender::); } - pub(crate) fn register_handle_provider(&self, handle_provider: AssetHandleProvider) { - self.write_infos() - .handle_providers - .insert(handle_provider.type_id, handle_provider); - } - /// Returns the registered [`AssetLoader`] associated with the given extension, if it exists. pub async fn get_asset_loader_with_extension( &self, @@ -319,7 +311,7 @@ impl AssetServer { /// Begins loading an [`Asset`] of type `A` stored at `path`. This will not block on the asset load. Instead, /// it returns a "strong" [`Handle`]. When the [`Asset`] is loaded (and enters [`LoadState::Loaded`]), it will be added to the - /// associated [`Assets`] resource. + /// associated [`AssetData`](crate::AssetData) for that handle. /// /// Note that if the asset at this path is already loaded, this function will return the existing handle, /// and will not waste work spawning a new load task. @@ -357,7 +349,7 @@ impl AssetServer { /// ``` /// /// You can check the asset's load state by reading [`AssetEvent`] events, calling [`AssetServer::load_state`], or checking - /// the [`Assets`] storage to see if the [`Asset`] exists yet. + /// the [`Assets`](crate::Assets) system param to see if the [`Asset`] exists yet. /// /// The asset load will fail and an error will be printed to the logs if the asset stored at `path` is not of type `A`. #[must_use = "not using the returned strong handle may result in the unexpected release of the asset"] @@ -399,7 +391,7 @@ impl AssetServer { /// The guard item is dropped when either the asset is loaded or loading has failed. /// /// This function returns a "strong" [`Handle`]. When the [`Asset`] is loaded (and enters [`LoadState::Loaded`]), it will be added to the - /// associated [`Assets`] resource. + /// associated [`AssetData`](crate::AssetData). /// /// The guard item should notify the caller in its [`Drop`] implementation. See example `multi_asset_sync`. /// Synchronously this can be a [`Arc`] that decrements its counter, asynchronously this can be a `Barrier`. @@ -407,7 +399,7 @@ impl AssetServer { /// multiple files, sub-assets referenced by the main asset might still be loading, depend on the implementation of the [`AssetLoader`]. /// /// Additionally, you can check the asset's load state by reading [`AssetEvent`] events, calling [`AssetServer::load_state`], or checking - /// the [`Assets`] storage to see if the [`Asset`] exists yet. + /// the [`Assets`](crate::Assets) system param to see if the [`Asset`] exists yet. /// /// The asset load will fail and an error will be printed to the logs if the asset stored at `path` is not of type `A`. #[deprecated(note = "Use `asset_server.load_builder().with_guard(guard).load(path)` instead")] @@ -530,7 +522,6 @@ impl AssetServer { &self, path: impl Into>, type_id: TypeId, - type_name: Option<&str>, meta_transform: Option, guard: G, override_unapproved: bool, @@ -558,9 +549,12 @@ impl AssetServer { let (handle, should_load) = infos.get_or_create_path_handle_erased( path.clone(), type_id, - type_name, HandleLoadingMode::Request, meta_transform, + &mut RemoteHandleBuilder { + remote_allocator: &self.data.remote_allocator, + asset_event_sender: &self.data.asset_event_sender, + }, ); if should_load { @@ -598,9 +592,7 @@ impl AssetServer { #[cfg(not(any(target_arch = "wasm32", not(feature = "multi_threaded"))))] { let mut infos = infos; - infos - .pending_tasks - .insert((&handle).try_into().unwrap(), task); + infos.pending_tasks.insert(handle.entity().unwrap(), task); } #[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] @@ -653,12 +645,17 @@ impl AssetServer { path.clone().with_source(untyped_source), HandleLoadingMode::Request, meta_transform, + &mut RemoteHandleBuilder { + remote_allocator: &self.data.remote_allocator, + asset_event_sender: &self.data.asset_event_sender, + }, ); if !should_load { return handle; } - let index = (&handle).try_into().unwrap(); + // `get_or_create_path_handle` always returns a strong handle. + let entity = handle.entity().unwrap(); infos.stats.started_load_tasks += 1; @@ -676,14 +673,15 @@ impl AssetServer { h.expect("handle must be returned, since we didn't pass in an input handle") }) { Ok(handle) => server.send_asset_event(InternalAssetEvent::Loaded { - index, + entity, loaded_asset: LoadedAsset::new_with_dependencies(LoadedUntypedAsset { handle }) .into(), }), Err(err) => { error!("{err}"); server.send_asset_event(InternalAssetEvent::Failed { - index, + entity, + type_id: TypeId::of::(), path: path_clone, error: err, }); @@ -693,7 +691,7 @@ impl AssetServer { }); #[cfg(not(any(target_arch = "wasm32", not(feature = "multi_threaded"))))] - infos.pending_tasks.insert(index, task); + infos.pending_tasks.insert(entity, task); #[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] task.detach(); @@ -714,7 +712,7 @@ impl AssetServer { /// #[derive(Resource)] /// struct LoadingUntypedHandle(Handle); /// - /// fn resolve_loaded_untyped_handle(loading_handle: Res, loaded_untyped_assets: Res>) { + /// fn resolve_loaded_untyped_handle(loading_handle: Res, loaded_untyped_assets: Assets) { /// if let Some(loaded_untyped_asset) = loaded_untyped_assets.get(&loading_handle.0) { /// let handle = loaded_untyped_asset.handle.clone(); /// // continue working with `handle` which points to the asset at the originally requested path @@ -757,7 +755,8 @@ impl AssetServer { // we cannot find the meta and loader if let Some(handle) = &input_handle { self.send_asset_event(InternalAssetEvent::Failed { - index: handle.try_into().unwrap(), + entity: handle.entity().unwrap(), + type_id: handle.type_id(), path: path.clone_owned(), error: e.clone(), }); @@ -768,13 +767,13 @@ impl AssetServer { (*meta_transform)(&mut *meta); } - let asset_id: Option; // The asset ID of the asset we are trying to load. + let asset_entity_and_type: Option<(AssetEntity, TypeId)>; // The entity of the asset we are trying to load. let fetched_handle; // The handle if one was looked up/created. let should_load; // Whether we need to load the asset. if let Some(input_handle) = input_handle { // This must have been created with `get_or_create_path_handle_internal` at some point, // which only produces Strong variant handles, so this is safe. - asset_id = Some((&input_handle).try_into().unwrap()); + asset_entity_and_type = Some((input_handle.entity().unwrap(), input_handle.type_id())); // In this case, we intentionally drop the input handle so we can cancel loading the // asset if the handle gets dropped (externally) before it finishes loading. fetched_handle = None; @@ -792,34 +791,34 @@ impl AssetServer { path.label().is_none().then(|| loader.asset_type_id()), HandleLoadingMode::Request, meta_transform, + &mut RemoteHandleBuilder { + remote_allocator: &self.data.remote_allocator, + asset_event_sender: &self.data.asset_event_sender, + }, ); - match unwrap_with_context( - result, - loader.asset_type_id(), - Some(loader.asset_type_name()), - ) { + match result { // We couldn't figure out the correct handle without its type ID (which can only // happen if we are loading a subasset). - None => { + Err(_) => { // We don't know the expected type since the subasset may have a different type // than the "root" asset (which is the type the loader will load). - asset_id = None; + asset_entity_and_type = None; fetched_handle = None; // If we couldn't find an appropriate handle, then the asset certainly needs to // be loaded. should_load = true; } - Some((handle, result_should_load)) => { + Ok((handle, result_should_load)) => { // `get_or_create_path_handle_internal` always returns Strong variant, so this // is safe. - asset_id = Some((&handle).try_into().unwrap()); + asset_entity_and_type = Some((handle.entity().unwrap(), handle.type_id())); fetched_handle = Some(handle); should_load = result_should_load; } } } // Verify that the expected type matches the loader's type. - if let Some(asset_type_id) = asset_id.map(|id| id.type_id) { + if let Some((_, asset_type_id)) = asset_entity_and_type { // If we are loading a subasset, then the subasset's type almost certainly doesn't match // the loader's type - and that's ok. if path.label().is_none() && asset_type_id != loader.asset_type_id() { @@ -851,20 +850,24 @@ impl AssetServer { .get_or_create_path_handle_erased( base_path.clone(), loader.asset_type_id(), - Some(loader.asset_type_name()), HandleLoadingMode::Force, None, + &mut RemoteHandleBuilder { + remote_allocator: &self.data.remote_allocator, + asset_event_sender: &self.data.asset_event_sender, + }, ) .0; ( // `get_or_create_path_handle_erased` always returns Strong variant, so this is // safe. - (&base_handle).try_into().unwrap(), + base_handle.entity().unwrap(), Some(base_handle), base_path, ) } else { - (asset_id.unwrap(), None, path.clone()) + let (entity, _) = asset_entity_and_type.unwrap(); + (entity, None, path.clone()) }; match self @@ -885,17 +888,18 @@ impl AssetServer { let labeled_asset = &loaded_asset.labeled_assets[*labeled_asset]; // If we know the requested type then check it // matches the labeled asset. - if let Some(asset_id) = asset_id - && asset_id.type_id != labeled_asset.handle.type_id() + if let Some((entity, type_id)) = asset_entity_and_type + && type_id != labeled_asset.handle.type_id() { let error = AssetLoadError::RequestedHandleTypeMismatch { path: path.clone(), - requested: asset_id.type_id, + requested: type_id, actual_asset_name: labeled_asset.asset.value.asset_type_name(), loader_name: loader.type_path(), }; self.send_asset_event(InternalAssetEvent::Failed { - index: asset_id, + entity, + type_id, error: error.clone(), path: path.into_owned(), }); @@ -915,9 +919,10 @@ impl AssetServer { label: label.to_string(), all_labels, }; - if let Some(asset_id) = asset_id { + if let Some((entity, type_id)) = asset_entity_and_type { self.send_asset_event(InternalAssetEvent::Failed { - index: asset_id, + entity, + type_id, error: error.clone(), path: path.into_owned(), }); @@ -930,15 +935,16 @@ impl AssetServer { }; self.send_asset_event(InternalAssetEvent::Loaded { - index: base_asset_id, + entity: base_asset_id, loaded_asset, }); Ok(final_handle) } Err(err) => { - if let Some(asset_id) = asset_id { + if let Some((entity, type_id)) = asset_entity_and_type { self.send_asset_event(InternalAssetEvent::Failed { - index: asset_id, + entity, + type_id, error: err.clone(), path: path.into_owned(), }); @@ -1003,7 +1009,8 @@ impl AssetServer { /// Queues a new asset to be tracked by the [`AssetServer`] and returns a [`Handle`] to it. This can be used to track /// dependencies of assets created at runtime. /// - /// After the asset has been fully loaded by the [`AssetServer`], it will show up in the relevant [`Assets`] storage. + /// After the asset has been fully loaded by the [`AssetServer`], it will show up in the + /// [`AssetData`](crate::AssetData) component on relevant asset entity. #[must_use = "not using the returned strong handle may result in the unexpected release of the asset"] pub fn add(&self, asset: A) -> Handle { self.load_asset(LoadedAsset::new_with_dependencies(asset)) @@ -1027,20 +1034,27 @@ impl AssetServer { let (handle, _) = self.write_infos().get_or_create_path_handle_erased( path, loaded_asset.asset_type_id(), - Some(loaded_asset.asset_type_name()), HandleLoadingMode::NotLoading, None, + &mut RemoteHandleBuilder { + remote_allocator: &self.data.remote_allocator, + asset_event_sender: &self.data.asset_event_sender, + }, ); handle } else { - self.write_infos().create_loading_handle_untyped( + let handle = self.write_infos().create_loading_handle_untyped( loaded_asset.asset_type_id(), - loaded_asset.asset_type_name(), - ) + &mut RemoteHandleBuilder { + remote_allocator: &self.data.remote_allocator, + asset_event_sender: &self.data.asset_event_sender, + }, + ); + UntypedHandle::Strong(handle) }; self.send_asset_event(InternalAssetEvent::Loaded { // `get_or_create_path_handle_erased` always returns Strong variant, so this is safe. - index: (&handle).try_into().unwrap(), + entity: handle.entity().unwrap(), loaded_asset, }); handle @@ -1049,21 +1063,26 @@ impl AssetServer { /// Queues a new asset to be tracked by the [`AssetServer`] and returns a [`Handle`] to it. This can be used to track /// dependencies of assets created at runtime. /// - /// After the asset has been fully loaded, it will show up in the relevant [`Assets`] storage. + /// After the asset has been fully loaded, it will show up in the relevant asset entity. #[must_use = "not using the returned strong handle may result in the unexpected release of the asset"] pub fn add_async( &self, future: impl Future> + Send + 'static, ) -> Handle { let mut infos = self.write_infos(); - let handle = infos.create_loading_handle_untyped(TypeId::of::(), type_name::()); + let handle = infos.create_loading_handle_untyped( + TypeId::of::(), + &mut RemoteHandleBuilder { + remote_allocator: &self.data.remote_allocator, + asset_event_sender: &self.data.asset_event_sender, + }, + ); // drop the lock on `AssetInfos` before spawning a task that may block on it in single-threaded #[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] drop(infos); - // `create_loading_handle_untyped` always returns a Strong variant, so this is safe. - let index = (&handle).try_into().unwrap(); + let entity = handle.entity; let event_sender = self.data.asset_event_sender.clone(); @@ -1073,7 +1092,7 @@ impl AssetServer { let loaded_asset = LoadedAsset::new_with_dependencies(asset).into(); event_sender .send(InternalAssetEvent::Loaded { - index, + entity, loaded_asset, }) .unwrap(); @@ -1085,7 +1104,8 @@ impl AssetServer { error!("{error}"); event_sender .send(InternalAssetEvent::Failed { - index, + entity, + type_id: TypeId::of::(), path: Default::default(), error: AssetLoadError::AddAsyncError(error), }) @@ -1095,12 +1115,12 @@ impl AssetServer { }); #[cfg(not(any(target_arch = "wasm32", not(feature = "multi_threaded"))))] - infos.pending_tasks.insert(index, task); + infos.pending_tasks.insert(entity, task); #[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] task.detach(); - handle.typed_debug_checked() + UntypedHandle::Strong(handle).typed_debug_checked() } /// Loads all assets from the specified folder recursively. The [`LoadedFolder`] asset (when it loads) will @@ -1118,19 +1138,23 @@ impl AssetServer { path.clone(), HandleLoadingMode::Request, None, + &mut RemoteHandleBuilder { + remote_allocator: &self.data.remote_allocator, + asset_event_sender: &self.data.asset_event_sender, + }, ); if !should_load { return handle; } // `get_or_create_path_handle` always returns a Strong variant, so this is safe. - let index = (&handle).try_into().unwrap(); + let entity = handle.entity().unwrap(); self.write_infos().stats.started_load_tasks += 1; - self.load_folder_internal(index, path); + self.load_folder_internal(entity, path); handle } - pub(crate) fn load_folder_internal(&self, index: ErasedAssetIndex, path: AssetPath) { + pub(crate) fn load_folder_internal(&self, entity: AssetEntity, path: AssetPath) { async fn load_folder<'a>( source: AssetSourceId<'static>, path: &'a Path, @@ -1198,7 +1222,7 @@ impl AssetServer { let mut handles = Vec::new(); match load_folder(source.id(), path.path(), asset_reader, &server, &mut handles).await { Ok(_) => server.send_asset_event(InternalAssetEvent::Loaded { - index, + entity, loaded_asset: LoadedAsset::new_with_dependencies( LoadedFolder { handles }, ) @@ -1206,7 +1230,7 @@ impl AssetServer { }), Err(err) => { error!("Failed to load folder. {err}"); - server.send_asset_event(InternalAssetEvent::Failed { index, error: err, path }); + server.send_asset_event(InternalAssetEvent::Failed { entity, type_id: TypeId::of::(), error: err, path }); }, } }) @@ -1222,11 +1246,8 @@ impl AssetServer { &self, id: impl Into, ) -> Option<(LoadState, DependencyLoadState, RecursiveDependencyLoadState)> { - let Ok(index) = id.into().try_into() else { - // Always say we don't have Uuid assets. - return None; - }; - self.read_infos().get(index).map(|i| { + let entity = self.data.uuid_map.resolve_entity(id.into()).ok()?; + self.read_infos().get(entity).map(|i| { ( i.load_state.clone(), i.dep_load_state.clone(), @@ -1241,11 +1262,8 @@ impl AssetServer { /// its dependencies or recursive dependencies, see [`AssetServer::get_dependency_load_state`] /// and [`AssetServer::get_recursive_dependency_load_state`] respectively. pub fn get_load_state(&self, id: impl Into) -> Option { - let Ok(index) = id.into().try_into() else { - // Always say we don't have Uuid assets. - return None; - }; - self.read_infos().get(index).map(|i| i.load_state.clone()) + let entity = self.data.uuid_map.resolve_entity(id.into()).ok()?; + self.read_infos().get(entity).map(|i| i.load_state.clone()) } /// Retrieves the [`DependencyLoadState`] of a given asset `id`'s dependencies. @@ -1257,12 +1275,9 @@ impl AssetServer { &self, id: impl Into, ) -> Option { - let Ok(index) = id.into().try_into() else { - // Always say we don't have Uuid assets. - return None; - }; + let entity = self.data.uuid_map.resolve_entity(id.into()).ok()?; self.read_infos() - .get(index) + .get(entity) .map(|i| i.dep_load_state.clone()) } @@ -1275,12 +1290,9 @@ impl AssetServer { &self, id: impl Into, ) -> Option { - let Ok(index) = id.into().try_into() else { - // Always say we don't have Uuid assets. - return None; - }; + let entity = self.data.uuid_map.resolve_entity(id.into()).ok()?; self.read_infos() - .get(index) + .get(entity) .map(|i| i.rec_dep_load_state.clone()) } @@ -1345,13 +1357,8 @@ impl AssetServer { pub fn are_dependencies_loaded(&self, value: &impl VisitAssetDependencies) -> bool { let infos = self.read_infos(); let mut loaded = true; - value.visit_dependencies(&mut |asset_id| { - let index = match asset_id { - // Ignore UUID assets - this effectively makes them considered loaded. - UntypedAssetId::Uuid { .. } => return, - UntypedAssetId::Index { type_id, index } => ErasedAssetIndex::new(index, type_id), - }; - let Some(info) = infos.get(index) else { + value.visit_dependencies(&mut |asset_entity| { + let Some(info) = infos.get(asset_entity) else { // If the asset ID is no longer valid, we consider that as not loaded. loaded = false; return; @@ -1370,13 +1377,8 @@ impl AssetServer { pub fn are_direct_dependencies_loaded(&self, value: &impl VisitAssetDependencies) -> bool { let infos = self.read_infos(); let mut loaded = true; - value.visit_dependencies(&mut |asset_id| { - let index = match asset_id { - // Ignore UUID assets - this effectively makes them considered loaded. - UntypedAssetId::Uuid { .. } => return, - UntypedAssetId::Index { type_id, index } => ErasedAssetIndex::new(index, type_id), - }; - let Some(info) = infos.get(index) else { + value.visit_dependencies(&mut |asset_entity| { + let Some(info) = infos.get(asset_entity) else { // If the asset ID is no longer valid, we consider that as not loaded. loaded = false; return; @@ -1396,60 +1398,52 @@ impl AssetServer { .map(UntypedHandle::typed_debug_checked) } - /// Get a `Handle` from an `AssetId`. + /// Get a `Handle` from an asset `Entity`. /// - /// This only returns `Some` if `id` is derived from a `Handle` that was + /// This only returns `Some` if `entity` is derived from a `Handle` that was /// loaded through an `AssetServer`, otherwise it returns `None`. /// - /// Consider using [`Assets::get_strong_handle`] in the case the `Handle` - /// comes from [`Assets::add`]. - pub fn get_id_handle(&self, id: AssetId) -> Option> { - self.get_id_handle_untyped(id.untyped()) + /// Consider using [`Assets::get_strong_handle`](crate::Assets::get_strong_handle) in the case + /// the `Handle` comes from [`AssetCommands::spawn_asset`](crate::AssetCommands::spawn_asset). + // TODO: Consider if we care to allow users to get handles outside the World. + pub fn get_entity_handle(&self, entity: AssetEntity) -> Option> { + self.get_entity_handle_untyped(entity) .map(UntypedHandle::typed) } /// Get an `UntypedHandle` from an `UntypedAssetId`. - /// See [`AssetServer::get_id_handle`] for details. - pub fn get_id_handle_untyped(&self, id: UntypedAssetId) -> Option { - let Ok(index) = id.try_into() else { - // Always say we don't have Uuid assets. - return None; - }; - self.read_infos().get_index_handle(index) + /// See [`AssetServer::get_entity_handle`] for details. + pub fn get_entity_handle_untyped(&self, entity: AssetEntity) -> Option { + self.read_infos().get_index_handle(entity) } /// Returns `true` if the given `id` corresponds to an asset that is managed by this [`AssetServer`]. /// Otherwise, returns `false`. pub fn is_managed(&self, id: impl Into) -> bool { - let Ok(index) = id.into().try_into() else { - // Always say we don't have Uuid assets. + let Ok(entity) = self.data.uuid_map.resolve_entity(id.into()) else { return false; }; - self.read_infos().contains_key(index) + self.read_infos().contains_key(entity) } /// Returns an active untyped asset id for the given path, if the asset at the given path has already started loading, /// or is still "alive". /// Returns the first ID in the event of multiple assets being registered against a single path. /// - /// # See also - /// [`get_path_ids`][Self::get_path_ids] for all handles. - pub fn get_path_id<'a>(&self, path: impl Into>) -> Option { + /// See also [`get_path_entities`][Self::get_path_entities] for all handles. + pub fn get_path_entity<'a>(&self, path: impl Into>) -> Option { let infos = self.read_infos(); let path = path.into(); - let mut ids = infos.get_path_indices(&path); - ids.next().map(Into::into) + let mut ids = infos.get_path_entities(&path); + ids.next() } /// Returns all active untyped asset IDs for the given path, if the assets at the given path have already started loading, /// or are still "alive". /// Multiple IDs will be returned in the event that a single path is used by multiple [`AssetLoader`]'s. - pub fn get_path_ids<'a>(&self, path: impl Into>) -> Vec { + pub fn get_path_entities<'a>(&self, path: impl Into>) -> Vec { let path = path.into(); - self.read_infos() - .get_path_indices(&path) - .map(Into::into) - .collect() + self.read_infos().get_path_entities(&path).collect() } /// Returns an active untyped handle for the given path, if the asset at the given path has already started loading, @@ -1485,12 +1479,11 @@ impl AssetServer { /// Returns the path for the given `id`, if it has one. pub fn get_path(&self, id: impl Into) -> Option> { - let Ok(index) = id.into().try_into() else { - // Always say we don't have Uuid assets. + let Ok(entity) = self.data.uuid_map.resolve_entity(id.into()) else { return None; }; let infos = self.read_infos(); - let info = infos.get(index)?; + let info = infos.get(entity)?; Some(info.path.as_ref()?.clone()) } @@ -1516,7 +1509,6 @@ impl AssetServer { self.get_or_create_path_handle_erased( path.into().into_owned(), TypeId::of::(), - Some(type_name::()), meta_transform, ) .typed_unchecked() @@ -1530,16 +1522,18 @@ impl AssetServer { &self, path: impl Into>, type_id: TypeId, - type_name: Option<&str>, meta_transform: Option, ) -> UntypedHandle { self.write_infos() .get_or_create_path_handle_erased( path.into().into_owned(), type_id, - type_name, HandleLoadingMode::NotLoading, meta_transform, + &mut RemoteHandleBuilder { + remote_allocator: &self.data.remote_allocator, + asset_event_sender: &self.data.asset_event_sender, + }, ) .0 } @@ -1685,6 +1679,33 @@ impl AssetServer { }) } + /// Create a fake handle for cases where we need a handle, but we don't ever need to actually + /// spawn the asset. + pub(crate) fn create_fake_handle( + &self, + type_id: TypeId, + path: Option>, + meta_transform: Option, + ) -> UntypedHandle { + let entity = AssetEntity::new_unchecked(self.data.remote_allocator.alloc()); + let _ = self + .data + .asset_event_sender + .send(InternalAssetEvent::FakeHandleCreated { entity }); + // Create a fake channel to send the drop event - we despawn the entity as soon as possible, + // so sending a drop event to the real channel just clutters things more. It's ok that the + // channel is immediately closed, since sending the drop event is a best-effort action (and + // ignores a closed sender). + let (fake_sender, _) = crossbeam_channel::bounded(0); + UntypedHandle::Strong(Arc::new(StrongHandle { + entity, + type_id, + meta_transform, + path, + drop_sender: fake_sender, + })) + } + /// Returns a future that will suspend until the specified asset and its dependencies finish /// loading. /// @@ -1698,7 +1719,11 @@ impl AssetServer { // which ensures the handle won't be dropped while waiting for the asset. handle: &Handle, ) -> Result<(), WaitForAssetError> { - self.wait_for_asset_id(handle.id().untyped()).await + let Some(entity) = handle.entity() else { + // Always say we aren't loading Uuid assets. + return Err(WaitForAssetError::NotLoaded); + }; + self.wait_for_asset_entity(entity).await } /// Returns a future that will suspend until the specified asset and its dependencies finish @@ -1714,7 +1739,11 @@ impl AssetServer { // which ensures the handle won't be dropped while waiting for the asset. handle: &UntypedHandle, ) -> Result<(), WaitForAssetError> { - self.wait_for_asset_id(handle.id()).await + let Some(entity) = handle.entity() else { + // Always say we aren't loading Uuid assets. + return Err(WaitForAssetError::NotLoaded); + }; + self.wait_for_asset_entity(entity).await } /// Returns a future that will suspend until the specified asset and its dependencies finish @@ -1736,26 +1765,23 @@ impl AssetServer { /// /// This will return an error if the asset or any of its dependencies fail to load, /// or if the asset has not been queued up to be loaded. - pub async fn wait_for_asset_id( + pub async fn wait_for_asset_entity( &self, - id: impl Into, + entity: AssetEntity, ) -> Result<(), WaitForAssetError> { - let Ok(index) = id.into().try_into() else { - // Always say we aren't loading Uuid assets. - return Err(WaitForAssetError::NotLoaded); - }; - core::future::poll_fn(move |cx| self.wait_for_asset_id_poll_fn(cx, index)).await + core::future::poll_fn(move |cx| self.wait_for_asset_entity_poll_fn(cx, entity)).await } - /// Used by [`wait_for_asset_id`](AssetServer::wait_for_asset_id) in [`poll_fn`](core::future::poll_fn). - fn wait_for_asset_id_poll_fn( + /// Used by [`wait_for_asset_entity`](AssetServer::wait_for_asset_entity) in + /// [`poll_fn`](core::future::poll_fn). + fn wait_for_asset_entity_poll_fn( &self, cx: &mut core::task::Context<'_>, - index: ErasedAssetIndex, + entity: AssetEntity, ) -> Poll> { let infos = self.read_infos(); - let Some(info) = infos.get(index) else { + let Some(info) = infos.get(entity) else { return Poll::Ready(Err(WaitForAssetError::NotLoaded)); }; @@ -1783,7 +1809,7 @@ impl AssetServer { self.write_infos() }; - let Some(info) = infos.get_mut(index) else { + let Some(info) = infos.get_mut(entity) else { return Poll::Ready(Err(WaitForAssetError::NotLoaded)); }; @@ -1945,9 +1971,9 @@ impl<'a> LoadBuilder<'a> { self } - /// Begins loading an [`Asset`] of type `A` stored at `path`. This will not block on the asset load. Instead, - /// it returns a "strong" [`Handle`]. When the [`Asset`] is loaded (and enters [`LoadState::Loaded`]), it will be added to the - /// associated [`Assets`] resource. + /// Begins loading an [`Asset`] of type `A` stored at `path`. This will not block on the asset + /// load. Instead, it returns a "strong" [`Handle`]. When the [`Asset`] is loaded (and enters + /// [`LoadState::Loaded`]), it will be inserted into the world. /// /// Note that if the asset at this path is already loaded, this function will return the existing handle, /// and will not waste work spawning a new load task. @@ -1956,7 +1982,7 @@ impl<'a> LoadBuilder<'a> { /// builder. See its docs for more details. #[must_use = "not using the returned strong handle may result in the unexpected release of the asset"] pub fn load<'b, A: Asset>(self, asset_path: impl Into>) -> Handle { - self.load_typed_internal(TypeId::of::(), Some(type_name::()), asset_path.into()) + self.load_typed_internal(TypeId::of::(), asset_path.into()) .typed_unchecked() } @@ -1968,7 +1994,7 @@ impl<'a> LoadBuilder<'a> { type_id: TypeId, asset_path: impl Into>, ) -> UntypedHandle { - self.load_typed_internal(type_id, None, asset_path.into()) + self.load_typed_internal(type_id, asset_path.into()) } /// Load an asset without knowing its type. The method returns a handle to a [`LoadedUntypedAsset`]. @@ -1984,7 +2010,7 @@ impl<'a> LoadBuilder<'a> { /// #[derive(Resource)] /// struct LoadingUntypedHandle(Handle); /// - /// fn resolve_loaded_untyped_handle(loading_handle: Res, loaded_untyped_assets: Res>) { + /// fn resolve_loaded_untyped_handle(loading_handle: Res, loaded_untyped_assets: Assets) { /// if let Some(loaded_untyped_asset) = loaded_untyped_assets.get(&loading_handle.0) { /// let handle = loaded_untyped_asset.handle.clone(); /// // continue working with `handle` which points to the asset at the originally requested path @@ -2035,16 +2061,10 @@ impl<'a> LoadBuilder<'a> { /// Begins a (deferred) load for an asset with the given `type_id` and `type_name`. #[must_use = "not using the returned strong handle may result in the unexpected release of the asset"] - fn load_typed_internal( - self, - type_id: TypeId, - type_name: Option<&str>, - asset_path: AssetPath<'_>, - ) -> UntypedHandle { + fn load_typed_internal(self, type_id: TypeId, asset_path: AssetPath<'_>) -> UntypedHandle { self.asset_server.load_with_meta_transform( asset_path, type_id, - type_name, self.meta_transform, self.guard, self.override_unapproved, @@ -2060,35 +2080,68 @@ pub fn handle_internal_asset_events(world: &mut World) { let mut untyped_failures = var_name; for event in server.data.asset_event_receiver.try_iter() { match event { + InternalAssetEvent::Spawned { handle, entity, remote } => { + if remote { + if !world + .entity_allocator() + .has_remote_allocator(&server.data.remote_allocator) { + warn!("The entity {:?} was remotely allocated, but the world has a different remote allocator. This entity does not correspond to this world!", entity); + continue; + } + + // TODO: What if someone drops the asset handle before this is spawned? + world.spawn_empty_at(entity.raw_entity()).unwrap(); + } + let Some(handle) = handle.upgrade() else { + // There's no point in inserting more components since this asset will be + // despawned anyway. But make sure to clear the asset from the server + // metadata. + infos.process_handle_drop(entity); + continue; + }; + // Try to setup the asset, but if the asset has already been despawned, that's + // ok. Just ignore it. + if setup_asset(world, &handle).is_err() { + // The handle was dropped before we could even do anything. + infos.process_handle_drop(entity); + continue; + }; + world.entity_mut(entity.raw_entity()).insert(AssetServerManaged); + } InternalAssetEvent::Loaded { - index, + entity, loaded_asset, } => { infos.process_asset_load( - index, + entity, loaded_asset, world, &server.data.asset_event_sender, ); } - InternalAssetEvent::LoadedWithDependencies { index } => { + InternalAssetEvent::LoadedWithDependencies { entity, type_id } => { let sender = infos .dependency_loaded_event_sender - .get(&index.type_id) + .get(&type_id) .expect("Asset event sender should exist"); - sender(world, index.index); - if let Some(info) = infos.get_mut(index) { + sender(world, entity); + if let Some(info) = infos.get_mut(entity) { for waker in info.waiting_tasks.drain(..) { waker.wake(); } } } - InternalAssetEvent::Failed { index, path, error } => { - infos.process_asset_fail(index, error.clone()); + InternalAssetEvent::Failed { + entity, + type_id, + path, + error, + } => { + infos.process_asset_fail(entity, error.clone()); // Send untyped failure event untyped_failures.push(UntypedAssetLoadFailedEvent { - id: index.into(), + id: UntypedAssetId::Entity { entity, type_id }, path: path.clone(), error: error.clone(), }); @@ -2096,9 +2149,22 @@ pub fn handle_internal_asset_events(world: &mut World) { // Send typed failure event let sender = infos .dependency_failed_event_sender - .get(&index.type_id) + .get(&type_id) .expect("Asset failed event sender should exist"); - sender(world, index.index, path, error); + sender(world, entity, path, error); + } + InternalAssetEvent::FakeHandleCreated { entity } => { + if !world + .entity_allocator() + .has_remote_allocator(&server.data.remote_allocator) { + warn!("The entity {:?} was remotely allocated, but the world has a different remote allocator. This entity does not correspond to this world!", entity); + continue; + } + // We allocated this entity, but we don't ever need to spawn it. So just despawn + // it to free the entity to the allocator. Note: we don't try to free the entity + // without despawn in case some "clever" user decides to `spawn_at` this entity. + // This is just some defensive programming. + let _ = world.try_despawn(entity.raw_entity()); } } } @@ -2202,8 +2268,8 @@ pub fn handle_internal_asset_events(world: &mut World) { for (handle, path) in folders_to_reload { // `get_path_handles` only returns Strong variants, so this is safe. - let index = (&handle).try_into().unwrap(); - server.load_folder_internal(index, path); + let entity = handle.entity().unwrap(); + server.load_folder_internal(entity, path); } for path in paths_to_reload { server.reload_internal(path, true); @@ -2229,18 +2295,28 @@ pub fn publish_asset_server_diagnostics( /// Internal events for asset load results pub(crate) enum InternalAssetEvent { + Spawned { + handle: Weak, + entity: AssetEntity, + remote: bool, + }, Loaded { - index: ErasedAssetIndex, + entity: AssetEntity, loaded_asset: ErasedLoadedAsset, }, LoadedWithDependencies { - index: ErasedAssetIndex, + entity: AssetEntity, + type_id: TypeId, }, Failed { - index: ErasedAssetIndex, + entity: AssetEntity, + type_id: TypeId, path: AssetPath<'static>, error: AssetLoadError, }, + FakeHandleCreated { + entity: AssetEntity, + }, } /// The load state of an asset. @@ -2525,3 +2601,34 @@ pub enum WriteDefaultMetaError { #[error("encountered HTTP status {0} when reading the existing meta file")] HttpErrorFromExistingMetaCheck(u16), } + +struct RemoteHandleBuilder<'a> { + remote_allocator: &'a RemoteAllocator, + asset_event_sender: &'a Sender, +} + +impl HandleBuilder for RemoteHandleBuilder<'_> { + fn create_entity(&mut self) -> Entity { + self.remote_allocator.alloc() + } + + fn trigger_setup(&mut self, handle: &Arc) { + let _ = self.asset_event_sender.send(InternalAssetEvent::Spawned { + handle: Arc::downgrade(handle), + entity: handle.entity, + remote: true, + }); + } +} + +#[derive(Component)] +#[component(on_remove=remove_asset_server_metadata)] +struct AssetServerManaged; + +/// Removes the metadata for this asset from the asset server. +fn remove_asset_server_metadata(world: DeferredWorld, HookContext { entity, .. }: HookContext) { + world + .resource::() + .write_infos() + .process_handle_drop(AssetEntity::new_unchecked(entity)); +} diff --git a/crates/bevy_asset/src/uuid_map.rs b/crates/bevy_asset/src/uuid_map.rs new file mode 100644 index 0000000000000..7b6d093e980ef --- /dev/null +++ b/crates/bevy_asset/src/uuid_map.rs @@ -0,0 +1,131 @@ +use core::any::TypeId; + +use alloc::sync::Arc; + +use bevy_ecs::resource::Resource; +use bevy_platform::{ + collections::{hash_map::Entry, HashMap, HashSet}, + sync::{PoisonError, RwLock, RwLockReadGuard}, +}; +use bevy_utils::TypeIdMap; +use thiserror::Error; +use uuid::Uuid; + +use crate::{ + Asset, AssetEntity, EntityHandle, Handle, UntypedAssetId, UntypedEntityHandle, UntypedHandle, +}; + +/// Maps asset UUIDs to the asset handle assigned to it. +#[derive(Resource, Clone, Default)] +pub struct AssetUuidMap(Arc>>); + +#[derive(Default)] +pub(crate) struct AssetUuidMapInner { + pub(crate) uuid_to_handle: HashMap, + entity_to_uuids: HashMap>, +} + +impl AssetUuidMap { + /// Sets the handle that a UUID refers to. + pub fn set_uuid(&mut self, uuid: Uuid, handle: UntypedEntityHandle) { + let mut type_id_map = self.0.write().unwrap_or_else(PoisonError::into_inner); + let inner = type_id_map.entry(handle.0.type_id).or_default(); + let new_entity = handle.entity(); + match inner.uuid_to_handle.entry(uuid) { + Entry::Vacant(entry) => { + entry.insert(handle); + } + Entry::Occupied(mut entry) => { + let old_entity = entry.get().entity(); + inner + .entity_to_uuids + .get_mut(&old_entity) + .unwrap() + .remove(&uuid); + entry.insert(handle); + } + } + inner + .entity_to_uuids + .entry(new_entity) + .or_default() + .insert(uuid); + } + + /// Convenience function for accessing the internal uuid map. + pub(crate) fn read(&self) -> RwLockReadGuard<'_, TypeIdMap> { + self.0.read().unwrap_or_else(PoisonError::into_inner) + } + + /// Converts an untyped handle into the corresponding [`UntypedEntityHandle`]. + /// + /// For [`UntypedHandle::Strong`], this is a no-op. For [`UntypedHandle::Uuid`], this lookups + /// the corresponding UUID and returns [`Err`] if missing. + pub fn resolve_untyped_handle( + &self, + handle: UntypedHandle, + ) -> Result { + match handle { + UntypedHandle::Strong(inner) => Ok(UntypedEntityHandle(inner)), + UntypedHandle::Uuid { type_id, uuid } => self + .read() + .get(&type_id) + .and_then(|inner| inner.uuid_to_handle.get(&uuid)) + .cloned() + .ok_or(ResolveUuidError(uuid)), + } + } + + /// Converts a handle into the corresponding [`EntityHandle`]. + /// + /// For [`Handle::Strong`], this is a no-op. For [`Handle::Uuid`], this lookups the + /// corresponding UUID and returns [`Err`] if missing. + pub fn resolve_handle( + &self, + handle: Handle, + ) -> Result, ResolveUuidError> { + self.resolve_untyped_handle(handle.untyped()) + // It's safe to unwrap, since either the handle was just passed through, or we looked up + // the handle by its type ID, so the types must match. + .map(|handle| handle.try_typed().unwrap()) + } + + /// Converts an asset ID into the corresponding [`AssetEntity`]. + /// + /// This is the same as [`Self::resolve_handle`], but is slightly more efficient for + /// cases where you don't need the resolved handle. + pub fn resolve_entity( + &self, + id: impl Into, + ) -> Result { + match id.into() { + UntypedAssetId::Entity { entity, .. } => Ok(entity), + UntypedAssetId::Uuid { type_id, uuid } => self + .read() + .get(&type_id) + .and_then(|inner| inner.uuid_to_handle.get(&uuid)) + .map(|value| value.0.entity) + .ok_or(ResolveUuidError(uuid)), + } + } + + /// Returns a reverse mapping from an entity to all UUIDs that reference it. + pub(crate) fn entity_to_uuids( + &self, + entity: AssetEntity, + type_id: TypeId, + ) -> Option> { + Some( + self.read() + .get(&type_id)? + .entity_to_uuids + .get(&entity)? + .clone(), + ) + } +} + +/// An error while resolve a [`Uuid`] in the [`AssetUuidMap`]. +#[derive(Error, Debug)] +#[error("There is no asset handle assigned to uuid {0}")] +pub struct ResolveUuidError(pub Uuid); diff --git a/crates/bevy_audio/src/audio_output.rs b/crates/bevy_audio/src/audio_output.rs index 1b23ccee8a072..24021d04fb066 100644 --- a/crates/bevy_audio/src/audio_output.rs +++ b/crates/bevy_audio/src/audio_output.rs @@ -83,7 +83,7 @@ impl<'w, 's> EarPositions<'w, 's> { /// data is available, and creates/inserts the sink. pub(crate) fn play_queued_audio_system( audio_output: Res, - audio_sources: Res>, + audio_sources: Assets, global_volume: Res, query_nonplaying: Query< ( diff --git a/crates/bevy_camera/src/visibility/mod.rs b/crates/bevy_camera/src/visibility/mod.rs index 2bd1da2da4f7e..e7f467ef66ed8 100644 --- a/crates/bevy_camera/src/visibility/mod.rs +++ b/crates/bevy_camera/src/visibility/mod.rs @@ -556,7 +556,7 @@ pub struct NoAutoAabb; /// This system is used in system set [`VisibilitySystems::CalculateBounds`]. pub fn calculate_bounds( mut commands: Commands, - meshes: Res>, + meshes: Assets, new_aabb: Query< (Entity, &Mesh3d), ( @@ -594,8 +594,8 @@ pub fn calculate_bounds( // Update the `Aabb` component of all skinned mesh entities with a `DynamicSkinnedMeshBounds` // component. fn update_skinned_mesh_bounds( - inverse_bindposes_assets: Res>, - mesh_assets: Res>, + inverse_bindposes_assets: Assets, + mesh_assets: Assets, mut mesh_entities: Query< (&mut Aabb, &Mesh3d, &SkinnedMesh, Option<&GlobalTransform>), With, diff --git a/crates/bevy_core_pipeline/src/mip_generation/mod.rs b/crates/bevy_core_pipeline/src/mip_generation/mod.rs index 514e0c829ae6a..a0f5ef94cf3cf 100644 --- a/crates/bevy_core_pipeline/src/mip_generation/mod.rs +++ b/crates/bevy_core_pipeline/src/mip_generation/mod.rs @@ -20,7 +20,7 @@ use crate::prepass::node::late_prepass; use crate::schedule::{Core3d, Core3dSystems}; use bevy_app::{App, Plugin}; -use bevy_asset::{embedded_asset, load_embedded_asset, AssetId, Assets, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetId, DirectAssetAccessExt, Handle}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ prelude::resource_exists, @@ -263,7 +263,6 @@ impl Plugin for MipGenerationPlugin { // specialized for each texture format by replacing `##TEXTURE_FORMAT##` // with each possible format. // When we have WESL, we should probably revisit this. - let mut shader_assets = app.world_mut().resource_mut::>(); let shader_template_source = include_str!("downsample.wgsl"); let general_shaders: HashMap<_, _> = TEXTURE_FORMATS .iter() @@ -272,7 +271,8 @@ impl Plugin for MipGenerationPlugin { shader_template_source.replace("##TEXTURE_FORMAT##", identifier); ( *target_format, - shader_assets.add(Shader::from_wgsl(shader_source, "downsample.wgsl")), + app.world_mut() + .spawn_asset(Shader::from_wgsl(shader_source, "downsample.wgsl")), ) }) .collect(); diff --git a/crates/bevy_core_pipeline/src/tonemapping/mod.rs b/crates/bevy_core_pipeline/src/tonemapping/mod.rs index d1fa63b830162..9dee114e3e924 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/mod.rs +++ b/crates/bevy_core_pipeline/src/tonemapping/mod.rs @@ -1,6 +1,7 @@ use bevy_app::prelude::*; use bevy_asset::{ - embedded_asset, load_embedded_asset, AssetServer, Assets, Handle, RenderAssetUsages, + embedded_asset, load_embedded_asset, AssetServer, DirectAssetAccessExt, Handle, + RenderAssetUsages, }; use bevy_camera::Camera; use bevy_ecs::prelude::*; @@ -49,20 +50,18 @@ impl Plugin for TonemappingPlugin { embedded_asset!(app, "tonemapping.wgsl"); if !app.world().is_resource_added::() { - let mut images = app.world_mut().resource_mut::>(); - #[cfg(feature = "tonemapping_luts")] let tonemapping_luts = { TonemappingLuts { - blender_filmic: images.add(setup_tonemapping_lut_image( + blender_filmic: app.world_mut().spawn_asset(setup_tonemapping_lut_image( include_bytes!("luts/Blender_-11_12.ktx2"), ImageType::Extension("ktx2"), )), - agx: images.add(setup_tonemapping_lut_image( + agx: app.world_mut().spawn_asset(setup_tonemapping_lut_image( include_bytes!("luts/AgX-default_contrast.ktx2"), ImageType::Extension("ktx2"), )), - tony_mc_mapface: images.add(setup_tonemapping_lut_image( + tony_mc_mapface: app.world_mut().spawn_asset(setup_tonemapping_lut_image( include_bytes!("luts/tony_mc_mapface.ktx2"), ImageType::Extension("ktx2"), )), @@ -71,7 +70,7 @@ impl Plugin for TonemappingPlugin { #[cfg(not(feature = "tonemapping_luts"))] let tonemapping_luts = { - let placeholder = images.add(lut_placeholder()); + let placeholder = app.world_mut().spawn_asset(lut_placeholder()); TonemappingLuts { blender_filmic: placeholder.clone(), agx: placeholder.clone(), diff --git a/crates/bevy_dev_tools/src/fps_overlay.rs b/crates/bevy_dev_tools/src/fps_overlay.rs index 7af5b8e8b2b00..c14d87b34366c 100644 --- a/crates/bevy_dev_tools/src/fps_overlay.rs +++ b/crates/bevy_dev_tools/src/fps_overlay.rs @@ -1,7 +1,7 @@ //! Module containing logic for FPS overlay. use bevy_app::{Plugin, Startup, Update}; -use bevy_asset::Assets; +use bevy_asset::AssetCommands; use bevy_color::Color; use bevy_diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin}; use bevy_ecs::{ @@ -11,7 +11,7 @@ use bevy_ecs::{ reflect::ReflectResource, resource::Resource, schedule::{common_conditions::resource_changed, IntoScheduleConfigs, SystemSet}, - system::{Commands, Query, Res, ResMut, Single}, + system::{Commands, Query, Res, Single}, }; use bevy_picking::Pickable; use bevy_reflect::Reflect; @@ -183,10 +183,7 @@ fn setup( all(target_arch = "wasm32", not(feature = "webgpu")), expect(unused, reason = "Unused variables in wasm32 without webgpu feature") )] - (mut frame_time_graph_materials, mut buffers): ( - ResMut>, - ResMut>, - ), + mut asset_commands: AssetCommands, ) { commands .spawn(( @@ -222,6 +219,13 @@ fn setup( { // Todo: Needs a better design that works with responsive sizing. let font_size = 20.; + let values = asset_commands.spawn_asset(ShaderBuffer { + // Initialize with dummy data because the default (`data: None`) will + // cause a panic in the shader if the frame time graph is constructed + // with `enabled: false`. + data: Some(vec![0, 0, 0, 0]), + ..Default::default() + }); p.spawn(( Node { width: Val::Px(font_size * FRAME_TIME_GRAPH_WIDTH_SCALE), @@ -234,14 +238,8 @@ fn setup( ..Default::default() }, Pickable::IGNORE, - MaterialNode::from(frame_time_graph_materials.add(FrametimeGraphMaterial { - values: buffers.add(ShaderBuffer { - // Initialize with dummy data because the default (`data: None`) will - // cause a panic in the shader if the frame time graph is constructed - // with `enabled: false`. - data: Some(vec![0, 0, 0, 0]), - ..Default::default() - }), + MaterialNode::from(asset_commands.spawn_asset(FrametimeGraphMaterial { + values, config: FrameTimeGraphConfigUniform::new( overlay_config.frame_time_graph_config.target_fps, overlay_config.frame_time_graph_config.min_fps, diff --git a/crates/bevy_dev_tools/src/frame_time_graph/mod.rs b/crates/bevy_dev_tools/src/frame_time_graph/mod.rs index bd3568d57ec34..f4df3dc39306e 100644 --- a/crates/bevy_dev_tools/src/frame_time_graph/mod.rs +++ b/crates/bevy_dev_tools/src/frame_time_graph/mod.rs @@ -1,12 +1,9 @@ //! Module containing logic for the frame time graph use bevy_app::{Plugin, Update}; -use bevy_asset::{load_internal_asset, uuid_handle, Asset, Assets, Handle}; +use bevy_asset::{load_internal_asset, uuid_handle, Asset, Assets, AssetsMut, Handle}; use bevy_diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin}; -use bevy_ecs::{ - schedule::IntoScheduleConfigs, - system::{Res, ResMut}, -}; +use bevy_ecs::{schedule::IntoScheduleConfigs, system::Res}; use bevy_math::ops::log2; use bevy_reflect::TypePath; use bevy_render::{ @@ -97,8 +94,8 @@ impl UiMaterial for FrametimeGraphMaterial { /// A system that updates the frame time values sent to the frame time graph fn update_frame_time_values( - mut frame_time_graph_materials: ResMut>, - mut buffers: ResMut>, + frame_time_graph_materials: Assets, + mut buffers: AssetsMut, diagnostics_store: Res, config: Option>, ) { @@ -113,7 +110,7 @@ fn update_frame_time_values( // convert to millis .map(|x| *x as f32 / 1000.0) .collect::>(); - for (_, material) in frame_time_graph_materials.iter_mut() { + for (_, material) in frame_time_graph_materials.iter() { let mut buffer = buffers.get_mut(&material.values).unwrap(); buffer.set_data(frame_times.clone()); diff --git a/crates/bevy_dev_tools/src/world_asset_helpers.rs b/crates/bevy_dev_tools/src/world_asset_helpers.rs index 3bb681959a37d..f09d752db1c2b 100644 --- a/crates/bevy_dev_tools/src/world_asset_helpers.rs +++ b/crates/bevy_dev_tools/src/world_asset_helpers.rs @@ -1,6 +1,6 @@ //! This modules contains functions that can make working with [`WorldAsset`] easier -use bevy_asset::{Assets, Handle}; +use bevy_asset::{Assets, AssetsMut, Handle}; use bevy_ecs::system::SystemState; use bevy_mesh::{Mesh, Mesh3d}; use bevy_transform::helper::TransformHelper; @@ -8,8 +8,8 @@ use bevy_world_serialization::WorldAsset; /// Merge all the [`Mesh3d`] of a [`WorldAsset`] into a single [`Mesh`] pub fn merge_all_mesh_3d( - world_assets: &mut Assets, - meshes: &mut Assets, + world_assets: &mut AssetsMut, + meshes: &Assets, scene_handle: &Handle, ) -> Option { let mut scene = world_assets.get_mut(scene_handle)?; @@ -21,7 +21,7 @@ pub fn merge_all_mesh_3d( for entity_ref in scene.world.iter_entities() { let Some(mesh) = entity_ref .get::() - .and_then(|mesh3d| meshes.get(mesh3d)) + .and_then(|mesh3d| meshes.get(mesh3d.id())) else { continue; }; diff --git a/crates/bevy_feathers/src/alpha_pattern.rs b/crates/bevy_feathers/src/alpha_pattern.rs index 9ea904e67331e..c8020a184601c 100644 --- a/crates/bevy_feathers/src/alpha_pattern.rs +++ b/crates/bevy_feathers/src/alpha_pattern.rs @@ -1,5 +1,5 @@ use bevy_app::Plugin; -use bevy_asset::{Asset, Assets, Handle}; +use bevy_asset::{Asset, DirectAssetAccessExt, Handle}; use bevy_ecs::{ component::Component, lifecycle::Add, @@ -28,10 +28,7 @@ pub(crate) struct AlphaPatternResource(pub(crate) Handle); impl FromWorld for AlphaPatternResource { fn from_world(world: &mut bevy_ecs::world::World) -> Self { - let mut ui_materials = world - .get_resource_mut::>() - .unwrap(); - Self(ui_materials.add(AlphaPatternMaterial::default())) + Self(world.spawn_asset(AlphaPatternMaterial::default())) } } diff --git a/crates/bevy_feathers/src/controls/color_plane.rs b/crates/bevy_feathers/src/controls/color_plane.rs index 14a213bf22101..b674f44cd0d66 100644 --- a/crates/bevy_feathers/src/controls/color_plane.rs +++ b/crates/bevy_feathers/src/controls/color_plane.rs @@ -1,5 +1,5 @@ use bevy_app::{Plugin, PostUpdate}; -use bevy_asset::{Asset, Assets}; +use bevy_asset::{Asset, AssetCommands, AssetsMut}; use bevy_ecs::{ bundle::Bundle, children, @@ -9,7 +9,7 @@ use bevy_ecs::{ observer::On, query::{Changed, Has, Or, With}, reflect::ReflectComponent, - system::{Commands, Query, Res, ResMut}, + system::{Commands, Query, Res}, template::FromTemplate, }; use bevy_math::{Vec2, Vec3}; @@ -245,8 +245,9 @@ fn update_plane_color( q_children: Query<&Children>, q_material_node: Query<&MaterialNode>, mut q_node: Query<&mut Node>, - mut r_materials: ResMut>, mut commands: Commands, + mut r_materials: AssetsMut, + mut asset_commands: AssetCommands, ) { for (plane_ent, plane, plane_value) in q_color_plane.iter() { // Find the inner entity @@ -266,7 +267,7 @@ fn update_plane_color( } } else { // Insert new node component - let material = r_materials.add(ColorPlaneMaterial { + let material = asset_commands.spawn_asset(ColorPlaneMaterial { plane: *plane, fixed_channel: plane_value.0.z, #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index 212f04a13a780..74a12ade13ae1 100755 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -79,7 +79,7 @@ pub mod prelude { } use bevy_app::{App, FixedFirst, FixedLast, Last, Plugin, RunFixedMainLoop}; -use bevy_asset::{Asset, AssetApp, Assets, Handle}; +use bevy_asset::{Asset, AssetApp, AssetCommands, AssetsMut, Handle}; use bevy_color::{Color, Oklcha}; use bevy_ecs::{ prelude::Entity, @@ -287,7 +287,8 @@ pub struct GizmoMeshSystems; /// /// This also clears the default `GizmoStorage`. fn update_gizmo_meshes( - mut gizmo_assets: ResMut>, + mut gizmo_assets: AssetsMut, + mut asset_commands: AssetCommands, mut handles: ResMut, mut storage: ResMut>, ) { @@ -295,7 +296,7 @@ fn update_gizmo_meshes( handles.handles.insert(TypeId::of::(), None); } else if let Some(handle) = handles.handles.get_mut(&TypeId::of::()) { if let Some(handle) = handle { - let mut gizmo = gizmo_assets.get_mut(handle.id()).unwrap(); + let mut gizmo = gizmo_assets.get_mut(handle).unwrap(); gizmo.buffer.list_positions = mem::take(&mut storage.list_positions); gizmo.buffer.list_colors = mem::take(&mut storage.list_colors); @@ -314,7 +315,7 @@ fn update_gizmo_meshes( }, }; - *handle = Some(gizmo_assets.add(gizmo)); + *handle = Some(asset_commands.spawn_asset(gizmo)); } } } diff --git a/crates/bevy_gizmos/src/retained.rs b/crates/bevy_gizmos/src/retained.rs index 5ca6d63136974..a5a5e3aef3d68 100644 --- a/crates/bevy_gizmos/src/retained.rs +++ b/crates/bevy_gizmos/src/retained.rs @@ -43,14 +43,14 @@ impl DerefMut for GizmoAsset { /// # use bevy_math::prelude::*; /// fn system( /// mut commands: Commands, -/// mut gizmo_assets: ResMut>, +/// mut asset_commands: AssetCommands, /// ) { /// let mut gizmo = GizmoAsset::default(); /// /// gizmo.sphere(Vec3::ZERO, 1., RED); /// /// commands.spawn(Gizmo { -/// handle: gizmo_assets.add(gizmo), +/// handle: asset_commands.spawn_asset(gizmo), /// line_config: GizmoLineConfig { /// width: 4., /// ..default() diff --git a/crates/bevy_gizmos/src/skinned_mesh_bounds.rs b/crates/bevy_gizmos/src/skinned_mesh_bounds.rs index 2ec322a9976d5..2e749fd76754b 100644 --- a/crates/bevy_gizmos/src/skinned_mesh_bounds.rs +++ b/crates/bevy_gizmos/src/skinned_mesh_bounds.rs @@ -85,10 +85,10 @@ pub struct ShowSkinnedMeshBoundsGizmo { fn draw( color: Color, mesh: &Mesh3d, - mesh_assets: &Res>, + mesh_assets: &Assets, skinned_mesh: &SkinnedMesh, joint_entities: &Query<&GlobalTransform>, - inverse_bindposes_assets: &Res>, + inverse_bindposes_assets: &Assets, gizmos: &mut Gizmos, ) { if let Some(mesh_asset) = mesh_assets.get(mesh) @@ -118,8 +118,8 @@ fn draw_skinned_mesh_bounds( With, >, joint_entities: Query<&GlobalTransform>, - mesh_assets: Res>, - inverse_bindposes_assets: Res>, + mesh_assets: Assets, + inverse_bindposes_assets: Assets, mut gizmos: Gizmos, ) { for (mesh, skinned_mesh, gizmo) in mesh_entities { @@ -146,8 +146,8 @@ fn draw_all_skinned_mesh_bounds( ), >, joint_entities: Query<&GlobalTransform>, - mesh_assets: Res>, - inverse_bindposes_assets: Res>, + mesh_assets: Assets, + inverse_bindposes_assets: Assets, mut gizmos: Gizmos, ) { for (mesh, skinned_mesh) in mesh_entities { diff --git a/crates/bevy_gizmos_render/src/transform_gizmo_render.rs b/crates/bevy_gizmos_render/src/transform_gizmo_render.rs index 51d474bce1c85..9b0fbf44b65a5 100644 --- a/crates/bevy_gizmos_render/src/transform_gizmo_render.rs +++ b/crates/bevy_gizmos_render/src/transform_gizmo_render.rs @@ -4,7 +4,7 @@ //! on a separate [`RenderLayers`] to render gizmo meshes always-on-top. use bevy_app::{App, Plugin, PostUpdate, Startup}; -use bevy_asset::{Assets, Handle}; +use bevy_asset::{AssetCommands, AssetsMut, Handle}; use bevy_camera::{ visibility::{RenderLayers, Visibility}, Camera, Camera3d, @@ -16,7 +16,7 @@ use bevy_ecs::{ query::{Or, With, Without}, resource::Resource, schedule::IntoScheduleConfigs, - system::{Commands, Query, Res, ResMut}, + system::{Commands, Query, Res}, }; use bevy_math::{ primitives::{Cone, Cuboid, Cylinder, Torus}, @@ -145,11 +145,7 @@ fn inactive_color(color: Color) -> Color { ) } -fn spawn_gizmo_meshes( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { +fn spawn_gizmo_meshes(mut commands: Commands, mut asset_commands: AssetCommands) { let gizmo_layer = RenderLayers::layer(GIZMO_RENDER_LAYER); let colors = [COLOR_X, COLOR_Y, COLOR_Z, COLOR_VIEW]; @@ -159,22 +155,16 @@ fn spawn_gizmo_meshes( inactive_colors: colors.map(inactive_color), }; - // Helper: create a unique unlit material for a given axis - let mut make_mat = |axis: TransformGizmoAxis| { - materials.add(make_unlit_material( - colors[TransformGizmoMaterials::axis_index(axis)], - )) - }; - // Pre-create meshes - let shaft_mesh = meshes.add(Cylinder::new(SHAFT_RADIUS, SHAFT_LENGTH).mesh().build()); - let cone_mesh = meshes.add(Cone::new(CONE_RADIUS, CONE_HEIGHT).mesh().build()); - let scale_cube_mesh = meshes.add( + let shaft_mesh = + asset_commands.spawn_asset(Cylinder::new(SHAFT_RADIUS, SHAFT_LENGTH).mesh().build()); + let cone_mesh = asset_commands.spawn_asset(Cone::new(CONE_RADIUS, CONE_HEIGHT).mesh().build()); + let scale_cube_mesh = asset_commands.spawn_asset( Cuboid::new(SCALE_CUBE_SIZE, SCALE_CUBE_SIZE, SCALE_CUBE_SIZE) .mesh() .build(), ); - let rotate_torus_mesh = meshes.add( + let rotate_torus_mesh = asset_commands.spawn_asset( Torus { minor_radius: 0.015, major_radius: ROTATE_RING_RADIUS, @@ -182,7 +172,7 @@ fn spawn_gizmo_meshes( .mesh() .build(), ); - let view_circle_mesh = meshes.add( + let view_circle_mesh = asset_commands.spawn_asset( Torus { minor_radius: VIEW_CIRCLE_MINOR, major_radius: VIEW_CIRCLE_MAJOR, @@ -190,7 +180,7 @@ fn spawn_gizmo_meshes( .mesh() .build(), ); - let view_ring_mesh = meshes.add( + let view_ring_mesh = asset_commands.spawn_asset( Torus { minor_radius: VIEW_RING_MINOR, major_radius: VIEW_RING_MAJOR, @@ -233,6 +223,13 @@ fn spawn_gizmo_meshes( commands.entity(child).insert(ChildOf(root_entity)); }; + // Helper: create a unique unlit material for a given axis + let mut make_mat = |axis: TransformGizmoAxis| { + asset_commands.spawn_asset(make_unlit_material( + colors[TransformGizmoMaterials::axis_index(axis)], + )) + }; + // --- Translate mode --- for axis in [ TransformGizmoAxis::X, @@ -375,7 +372,7 @@ fn update_gizmo_meshes( ), Without, >, - mut std_materials: ResMut>, + mut std_materials: AssetsMut, mut overlay_cam: Query< &mut Transform, ( diff --git a/crates/bevy_gltf/src/lib.rs b/crates/bevy_gltf/src/lib.rs index ead576cb1992d..e1aaec532c2f6 100644 --- a/crates/bevy_gltf/src/lib.rs +++ b/crates/bevy_gltf/src/lib.rs @@ -58,7 +58,7 @@ //! fn spawn_gltf_objects( //! mut commands: Commands, //! helmet_scene: Res, -//! gltf_assets: Res>, +//! gltf_assets: Assets, //! mut loaded: Local, //! ) { //! // Only do this once diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index 14554608931d3..761f641f20b36 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -2123,19 +2123,19 @@ pub struct MorphTargetNames { mod test { use std::path::Path; - use crate::{Gltf, GltfAssetLabel, GltfMaterial, GltfNode, GltfSkin}; + use crate::{Gltf, GltfAssetLabel, GltfMaterial}; use bevy_app::{App, TaskPoolPlugin}; use bevy_asset::{ io::{ memory::{Dir, MemoryAssetReader}, AssetSourceBuilder, AssetSourceId, }, - AssetApp, AssetLoader, AssetPlugin, AssetServer, Assets, Handle, LoadContext, LoadState, + AssetApp, AssetLoader, AssetPlugin, AssetServer, DirectAssetAccessExt, Handle, LoadContext, + LoadState, }; use bevy_ecs::{resource::Resource, world::World}; use bevy_image::{Image, ImageLoaderSettings}; use bevy_log::LogPlugin; - use bevy_mesh::skinning::SkinnedMeshInverseBindposes; use bevy_mesh::MeshPlugin; use bevy_reflect::TypePath; use bevy_world_serialization::WorldSerializationPlugin; @@ -2224,17 +2224,16 @@ mod test { "#, ); let asset_server = app.world().resource::(); - let handle = asset_server.load(gltf_path); - let gltf_root_assets = app.world().resource::>(); - let gltf_node_assets = app.world().resource::>(); - let gltf_root = gltf_root_assets.get(&handle).unwrap(); + let handle = asset_server.load::(gltf_path); + let gltf_root = app.world().get_asset(handle.id()).unwrap(); assert!(gltf_root.nodes.len() == 1, "Single node"); assert!( gltf_root.named_nodes.contains_key("TestSingleNode"), "Named node is in named nodes" ); - let gltf_node = gltf_node_assets - .get(gltf_root.named_nodes.get("TestSingleNode").unwrap()) + let gltf_node = app + .world() + .get_asset(gltf_root.named_nodes.get("TestSingleNode").unwrap().id()) .unwrap(); assert_eq!(gltf_node.name, "TestSingleNode", "Correct name"); assert_eq!(gltf_node.index, 0, "Correct index"); @@ -2266,14 +2265,12 @@ mod test { "#, ); let asset_server = app.world().resource::(); - let handle = asset_server.load(gltf_path); - let gltf_root_assets = app.world().resource::>(); - let gltf_node_assets = app.world().resource::>(); - let gltf_root = gltf_root_assets.get(&handle).unwrap(); + let handle = asset_server.load::(gltf_path); + let gltf_root = app.world().get_asset(handle.id()).unwrap(); let result = gltf_root .nodes .iter() - .map(|h| gltf_node_assets.get(h).unwrap()) + .map(|h| app.world().get_asset(h.id()).unwrap()) .collect::>(); assert_eq!(result.len(), 2); assert_eq!(result[0].name, "l1"); @@ -2307,14 +2304,12 @@ mod test { "#, ); let asset_server = app.world().resource::(); - let handle = asset_server.load(gltf_path); - let gltf_root_assets = app.world().resource::>(); - let gltf_node_assets = app.world().resource::>(); - let gltf_root = gltf_root_assets.get(&handle).unwrap(); + let handle = asset_server.load::(gltf_path); + let gltf_root = app.world().get_asset(handle.id()).unwrap(); let result = gltf_root .nodes .iter() - .map(|h| gltf_node_assets.get(h).unwrap()) + .map(|h| app.world().get_asset(h.id()).unwrap()) .collect::>(); assert_eq!(result.len(), 2); assert_eq!(result[0].name, "l1"); @@ -2366,14 +2361,12 @@ mod test { "#, ); let asset_server = app.world().resource::(); - let handle = asset_server.load(gltf_path); - let gltf_root_assets = app.world().resource::>(); - let gltf_node_assets = app.world().resource::>(); - let gltf_root = gltf_root_assets.get(&handle).unwrap(); + let handle = asset_server.load::(gltf_path); + let gltf_root = app.world().get_asset(handle.id()).unwrap(); let result = gltf_root .nodes .iter() - .map(|h| gltf_node_assets.get(h).unwrap()) + .map(|h| app.world().get_asset(h.id()).unwrap()) .collect::>(); assert_eq!(result.len(), 7); assert_eq!(result[0].name, "l1"); @@ -2532,25 +2525,22 @@ mod test { "#, ); let asset_server = app.world().resource::(); - let handle = asset_server.load(gltf_path); - let gltf_root_assets = app.world().resource::>(); - let gltf_node_assets = app.world().resource::>(); - let gltf_skin_assets = app.world().resource::>(); - let gltf_inverse_bind_matrices = app - .world() - .resource::>(); - let gltf_root = gltf_root_assets.get(&handle).unwrap(); + let handle = asset_server.load::(gltf_path); + let gltf_root = app.world().get_asset(handle.id()).unwrap(); assert_eq!(gltf_root.skins.len(), 1); assert_eq!(gltf_root.nodes.len(), 3); - let skin = gltf_skin_assets.get(&gltf_root.skins[0]).unwrap(); + let skin = app.world().get_asset(gltf_root.skins[0].id()).unwrap(); assert_eq!(skin.joints.len(), 2); assert_eq!(skin.joints[0], gltf_root.nodes[1]); assert_eq!(skin.joints[1], gltf_root.nodes[2]); - assert!(gltf_inverse_bind_matrices.contains(&skin.inverse_bind_matrices)); + assert!(app + .world() + .get_asset(skin.inverse_bind_matrices.id()) + .is_some()); - let skinned_node = gltf_node_assets.get(&gltf_root.nodes[0]).unwrap(); + let skinned_node = app.world().get_asset(gltf_root.nodes[0].id()).unwrap(); assert_eq!(skinned_node.name, "skinned"); assert_eq!(skinned_node.children.len(), 2); assert_eq!(skinned_node.skin.as_ref(), Some(&gltf_root.skins[0])); diff --git a/crates/bevy_image/src/image.rs b/crates/bevy_image/src/image.rs index 2845536078d3e..ca3f948810df9 100644 --- a/crates/bevy_image/src/image.rs +++ b/crates/bevy_image/src/image.rs @@ -14,7 +14,9 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; #[cfg(feature = "serialize")] use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; -use bevy_asset::{uuid_handle, Asset, AssetApp, Assets, Handle, RenderAssetUsages}; +use bevy_asset::{ + uuid_handle, Asset, AssetApp, AssetId, DirectAssetAccessExt, Handle, RenderAssetUsages, +}; use bevy_color::{Color, ColorToComponents, Gray, LinearRgba, Srgba, Xyza}; use bevy_ecs::resource::Resource; use bevy_math::{AspectRatio, UVec2, UVec3, Vec2}; @@ -216,14 +218,12 @@ impl Plugin for ImagePlugin { #[cfg(feature = "bevy_reflect")] app.register_asset_reflect::(); - let mut image_assets = app.world_mut().resource_mut::>(); - - image_assets - .insert(&Handle::default(), Image::default()) - .unwrap(); - image_assets - .insert(&TRANSPARENT_IMAGE_HANDLE, Image::transparent()) - .unwrap(); + app.world_mut() + .spawn_uuid_asset::(AssetId::::DEFAULT_UUID, Image::default()); + app.world_mut().spawn_uuid_asset::( + TRANSPARENT_IMAGE_HANDLE.uuid().unwrap(), + Image::transparent(), + ); #[cfg(feature = "compressed_image_saver")] if let Some(processor) = app diff --git a/crates/bevy_image/src/saver.rs b/crates/bevy_image/src/saver.rs index e601326f0a3e3..c466d16013307 100644 --- a/crates/bevy_image/src/saver.rs +++ b/crates/bevy_image/src/saver.rs @@ -162,7 +162,7 @@ mod tests { AssetSourceBuilder, AssetSourceId, }, saver::{save_using_saver, SavedAsset}, - AssetApp, AssetPath, AssetPlugin, AssetServer, Assets, RenderAssetUsages, + AssetApp, AssetPath, AssetPlugin, AssetServer, DirectAssetAccessExt, RenderAssetUsages, }; use bevy_color::Srgba; use bevy_ecs::world::World; @@ -280,11 +280,7 @@ mod tests { let handle = asset_server.load::(asset_path); run_app_until(&mut app, |_| asset_server.is_loaded(&handle).then_some(())); - let loaded_image = app - .world() - .resource::>() - .get(&handle) - .unwrap(); + let loaded_image = app.world().get_asset(handle.id()).unwrap(); assert_eq!(loaded_image.size(), UVec2::new(WIDTH, WIDTH)); let compare_images = 'compare_images: { diff --git a/crates/bevy_image/src/texture_atlas_builder.rs b/crates/bevy_image/src/texture_atlas_builder.rs index a4a6da79024e0..ec269a04123f0 100644 --- a/crates/bevy_image/src/texture_atlas_builder.rs +++ b/crates/bevy_image/src/texture_atlas_builder.rs @@ -184,15 +184,15 @@ impl<'a> TextureAtlasBuilder<'a> { /// # use bevy_asset::*; /// # use bevy_image::prelude::*; /// - /// fn my_system(mut textures: ResMut>, mut layouts: ResMut>) { + /// fn my_system(mut asset_commands: AssetCommands) { /// // Declare your builder /// let mut builder = TextureAtlasBuilder::default(); /// // Customize it /// // ... /// // Build your texture and the atlas layout /// let (atlas_layout, atlas_sources, texture) = builder.build().unwrap(); - /// let texture = textures.add(texture); - /// let layout = layouts.add(atlas_layout); + /// let texture = asset_commands.spawn_asset(texture); + /// let layout = asset_commands.spawn_asset(atlas_layout); /// } /// ``` /// diff --git a/crates/bevy_light/src/atmosphere.rs b/crates/bevy_light/src/atmosphere.rs index 83f7b9ab4295b..8b369e02f277c 100644 --- a/crates/bevy_light/src/atmosphere.rs +++ b/crates/bevy_light/src/atmosphere.rs @@ -1,14 +1,10 @@ //! Provides types to specify atmosphere lighting, scattering terms, etc. use alloc::{borrow::Cow, sync::Arc}; -use bevy_asset::{Asset, AssetEvent, AssetId, Handle}; +use bevy_asset::{Asset, AssetEvent, AssetId, Assets, AssetsMut, Handle}; use bevy_color::{ColorToComponents, Gray, LinearRgba}; use bevy_ecs::{ - component::Component, - lifecycle::HookContext, - message::MessageReader, - system::{Res, ResMut}, - template::FromTemplate, + component::Component, lifecycle::HookContext, message::MessageReader, template::FromTemplate, world::DeferredWorld, }; use bevy_image::Image; @@ -547,8 +543,8 @@ impl Default for PhaseFunction { /// Resolves [`PhaseFunction::ChromaticTexture`] to [`PhaseFunction::ChromaticCurve`] when the image loads. pub fn extract_chromatic_phase_textures( mut reader: MessageReader>, - images: Res>, - mut scattering_media: ResMut>, + images: Assets, + mut scattering_media: AssetsMut, ) { let extract_ids: HashSet> = scattering_media .iter() @@ -597,7 +593,7 @@ pub fn extract_chromatic_phase_textures( let new_phase = PhaseFunction::from_chromatic_curve(curve); - for (_id, medium) in scattering_media.iter_mut() { + for (_id, mut medium) in scattering_media.iter_mut() { for term in medium.terms.iter_mut() { if let PhaseFunction::ChromaticTexture(handle) = &term.phase && handle.id() == *id diff --git a/crates/bevy_light/src/probe.rs b/crates/bevy_light/src/probe.rs index b5ba960c99076..93a9ae01c958d 100644 --- a/crates/bevy_light/src/probe.rs +++ b/crates/bevy_light/src/probe.rs @@ -1,4 +1,4 @@ -use bevy_asset::{Assets, Handle, HandleTemplate, RenderAssetUsages}; +use bevy_asset::{AssetCommands, Handle, HandleTemplate, RenderAssetUsages}; use bevy_camera::visibility::{self, ViewVisibility, Visibility, VisibilityClass}; use bevy_color::{Color, ColorToComponents, LinearRgba}; use bevy_ecs::prelude::*; @@ -140,20 +140,20 @@ pub struct EnvironmentMapLight { impl EnvironmentMapLight { /// An environment map with a uniform color, useful for uniform ambient lighting. - pub fn solid_color(assets: &mut Assets, color: impl Into) -> Self { + pub fn solid_color(asset_commands: &mut AssetCommands, color: impl Into) -> Self { let color = color.into(); - Self::hemispherical_gradient(assets, color, color, color) + Self::hemispherical_gradient(asset_commands, color, color, color) } /// An environment map with a hemispherical gradient, fading between the sky and ground colors /// at the horizon. Useful as a very simple 'sky'. pub fn hemispherical_gradient( - assets: &mut Assets, + asset_commands: &mut AssetCommands, top_color: impl Into, mid_color: impl Into, bottom_color: impl Into, ) -> Self { - let handle = assets.add(Self::hemispherical_gradient_cubemap( + let handle = asset_commands.spawn_asset(Self::hemispherical_gradient_cubemap( top_color.into(), mid_color.into(), bottom_color.into(), diff --git a/crates/bevy_mesh/src/components.rs b/crates/bevy_mesh/src/components.rs index 298cf5eb6f948..a991a64a2b0c5 100644 --- a/crates/bevy_mesh/src/components.rs +++ b/crates/bevy_mesh/src/components.rs @@ -28,8 +28,8 @@ use derive_more::derive::From; /// // Spawn an entity with a mesh using `ColorMaterial`. /// fn setup( /// mut commands: Commands, -/// mut meshes: ResMut>, -/// mut materials: ResMut>, +/// mut meshes: AssetsMut, +/// mut materials: AssetsMut, /// ) { /// commands.spawn(( /// Mesh2d(meshes.add(Circle::new(50.0))), @@ -82,8 +82,8 @@ impl AsAssetId for Mesh2d { /// // Spawn an entity with a mesh using `StandardMaterial`. /// fn setup( /// mut commands: Commands, -/// mut meshes: ResMut>, -/// mut materials: ResMut>, +/// mut meshes: AssetsMut, +/// mut materials: AssetsMut, /// ) { /// commands.spawn(( /// Mesh3d(meshes.add(Capsule3d::default())), diff --git a/crates/bevy_mesh/src/primitives/mod.rs b/crates/bevy_mesh/src/primitives/mod.rs index 02aa0f1caf41e..327b574207db0 100644 --- a/crates/bevy_mesh/src/primitives/mod.rs +++ b/crates/bevy_mesh/src/primitives/mod.rs @@ -5,17 +5,17 @@ //! that can be used to specify shape-specific configuration for creating the [`Mesh`]. //! //! ``` -//! # use bevy_asset::Assets; +//! # use bevy_asset::{Assets, AssetCommands}; //! # use bevy_ecs::prelude::ResMut; //! # use bevy_math::prelude::Circle; //! # use bevy_mesh::*; //! # -//! # fn setup(mut meshes: ResMut>) { +//! # fn setup(mut asset_commands: AssetCommands) { //! // Create circle mesh with default configuration -//! let circle = meshes.add(Circle { radius: 25.0 }); +//! let circle = asset_commands.spawn_asset(Mesh::from(Circle { radius: 25.0 })); //! //! // Specify number of vertices -//! let circle = meshes.add(Circle { radius: 25.0 }.mesh().resolution(64)); +//! let circle = asset_commands.spawn_asset(Mesh::from(Circle { radius: 25.0 }.mesh().resolution(64))); //! # } //! ``` diff --git a/crates/bevy_pbr/src/atmosphere/environment.rs b/crates/bevy_pbr/src/atmosphere/environment.rs index e19f39e15323b..78fc2325a780e 100644 --- a/crates/bevy_pbr/src/atmosphere/environment.rs +++ b/crates/bevy_pbr/src/atmosphere/environment.rs @@ -5,13 +5,13 @@ use crate::{ }, ExtractedAtmosphere, GpuAtmosphereSettings, GpuLights, LightMeta, ViewLightsUniformOffset, }; -use bevy_asset::{load_embedded_asset, AssetServer, Assets, Handle, RenderAssetUsages}; +use bevy_asset::{load_embedded_asset, AssetCommands, AssetServer, Handle, RenderAssetUsages}; use bevy_ecs::{ component::Component, entity::Entity, query::{With, Without}, resource::Resource, - system::{Commands, Query, Res, ResMut}, + system::{Commands, Query, Res}, template::FromTemplate, }; use bevy_image::Image; @@ -198,7 +198,7 @@ pub fn validate_environment_map_size(size: UVec2) -> UVec2 { pub fn prepare_atmosphere_probe_components( probes: Query<(Entity, &AtmosphereEnvironmentMapLight), (Without,)>, mut commands: Commands, - mut images: ResMut>, + mut asset_commands: AssetCommands, ) { for (entity, env_map_light) in &probes { // Create a cubemap image in the main world that we can reference @@ -225,7 +225,7 @@ pub fn prepare_atmosphere_probe_components( | TextureUsages::COPY_SRC; // Add the image to assets to get a handle - let environment_handle = images.add(environment_image); + let environment_handle = asset_commands.spawn_asset(environment_image); commands.entity(entity).insert(AtmosphereEnvironmentMap { environment_map: environment_handle.clone(), diff --git a/crates/bevy_pbr/src/decal/forward.rs b/crates/bevy_pbr/src/decal/forward.rs index c875bb7164257..d5d514740a46d 100644 --- a/crates/bevy_pbr/src/decal/forward.rs +++ b/crates/bevy_pbr/src/decal/forward.rs @@ -3,7 +3,7 @@ use crate::{ MaterialPlugin, StandardMaterial, }; use bevy_app::{App, Plugin}; -use bevy_asset::{Asset, Assets, Handle}; +use bevy_asset::{Asset, DirectAssetAccessExt, Handle}; use bevy_ecs::{ component::Component, lifecycle::HookContext, resource::Resource, world::DeferredWorld, }; @@ -29,7 +29,7 @@ impl Plugin for ForwardDecalPlugin { fn build(&self, app: &mut App) { load_shader_library!(app, "forward_decal.wgsl"); - let mesh = app.world_mut().resource_mut::>().add( + let mesh = app.world_mut().spawn_asset( Rectangle::from_size(Vec2::ONE) .mesh() .build() diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index c3ee7793dd3bb..6ab0ac76b0358 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -106,7 +106,7 @@ pub mod prelude { use crate::gpu::GpuClusteringPlugin; use crate::{deferred::DeferredPbrLightingPlugin, gpu::extract_clusters_for_gpu_clustering}; use bevy_app::prelude::*; -use bevy_asset::{AssetApp, AssetPath, Assets, Handle, RenderAssetUsages}; +use bevy_asset::{AssetApp, AssetId, AssetPath, DirectAssetAccessExt, Handle, RenderAssetUsages}; use bevy_core_pipeline::mip_generation::experimental::depth::early_downsample_depth; use bevy_core_pipeline::schedule::{Core3d, Core3dSystems}; use bevy_ecs::prelude::*; @@ -263,23 +263,19 @@ impl Plugin for PbrPlugin { } // Initialize the default material handle. - app.world_mut() - .resource_mut::>() - .insert( - &Handle::::default(), - StandardMaterial { - base_color: Color::srgb(1.0, 0.0, 0.5), - ..Default::default() - }, - ) - .unwrap(); + app.world_mut().spawn_uuid_asset::( + AssetId::<()>::DEFAULT_UUID, + StandardMaterial { + base_color: Color::srgb(1.0, 0.0, 0.5), + ..Default::default() + }, + ); let has_bluenoise = app .get_sub_app(RenderApp) .is_some_and(|render_app| render_app.world().is_resource_added::()); if !has_bluenoise { - let mut images = app.world_mut().resource_mut::>(); #[cfg(feature = "bluenoise_texture")] let handle = { let mut image = Image::from_buffer( @@ -292,11 +288,11 @@ impl Plugin for PbrPlugin { ) .expect("Failed to decode embedded blue-noise texture"); image.texture_descriptor.label = Some("bluenoise"); - images.add(image) + app.world_mut().spawn_asset(image) }; #[cfg(not(feature = "bluenoise_texture"))] - let handle = { images.add(stbn_placeholder()) }; + let handle = { app.world_mut().spawn_asset(stbn_placeholder()) }; if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app @@ -310,7 +306,6 @@ impl Plugin for PbrPlugin { .is_some_and(|render_app| render_app.world().is_resource_added::()); if !has_area_light_luts { - let mut images = app.world_mut().resource_mut::>(); #[cfg(feature = "area_light_luts")] let handle = { let mut image = Image::from_buffer( @@ -323,10 +318,10 @@ impl Plugin for PbrPlugin { ) .expect("Failed to decode embedded LTC LUTs"); image.texture_descriptor.label = Some("area_light_luts"); - images.add(image) + app.world_mut().spawn_asset(image) }; #[cfg(not(feature = "area_light_luts"))] - let handle = images.add(area_light_luts_placeholder()); + let handle = app.world_mut().spawn_asset(area_light_luts_placeholder()); let area_light_luts = AreaLightLuts { image: handle }; if let Some(render_app) = app.get_sub_app_mut(RenderApp) { @@ -340,7 +335,7 @@ impl Plugin for PbrPlugin { if !has_dfg_lut { #[cfg(feature = "dfg_lut")] - let texture = app.world_mut().resource_mut::>().add( + let texture = app.world_mut().spawn_asset( Image::from_buffer( include_bytes!("environment_map/dfg.ktx2"), bevy_image::ImageType::Extension("ktx2"), diff --git a/crates/bevy_pbr/src/light_probe/generate.rs b/crates/bevy_pbr/src/light_probe/generate.rs index fe4dd70ffdd59..8e8cd320a742e 100644 --- a/crates/bevy_pbr/src/light_probe/generate.rs +++ b/crates/bevy_pbr/src/light_probe/generate.rs @@ -12,7 +12,9 @@ //! For prefiltered environment maps, see [`bevy_light::EnvironmentMapLight`]. //! These components are intended to be added to a camera. use bevy_app::{App, Plugin, Update}; -use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Assets, RenderAssetUsages}; +use bevy_asset::{ + embedded_asset, load_embedded_asset, AssetCommands, AssetServer, Assets, RenderAssetUsages, +}; use bevy_core_pipeline::mip_generation::{self, DownsampleShaders, DownsamplingConstants}; use bevy_ecs::{ component::Component, @@ -1022,7 +1024,8 @@ pub fn filtering_system( /// System that generates an `EnvironmentMapLight` component based on the `GeneratedEnvironmentMapLight` component pub fn generate_environment_map_light( mut commands: Commands, - mut images: ResMut>, + mut asset_commands: AssetCommands, + images: Assets, query: Query<(Entity, &GeneratedEnvironmentMapLight), Without>, ) { for (entity, filtered_env_map) in &query { @@ -1068,7 +1071,7 @@ pub fn generate_environment_map_light( ..Default::default() }); - let diffuse_handle = images.add(diffuse); + let diffuse_handle = asset_commands.spawn_asset(diffuse); // Create a placeholder for the specular map. It matches the input cubemap resolution. let mut specular = Image::new_fill( @@ -1098,7 +1101,7 @@ pub fn generate_environment_map_light( ..Default::default() }); - let specular_handle = images.add(specular); + let specular_handle = asset_commands.spawn_asset(specular); // Add the EnvironmentMapLight component with the placeholder handles commands.entity(entity).insert(EnvironmentMapLight { diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 967ccda522ace..39d85e839c047 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -95,7 +95,7 @@ pub const MATERIAL_BIND_GROUP_INDEX: usize = 3; /// # use bevy_shader::ShaderRef; /// # use bevy_color::LinearRgba; /// # use bevy_color::palettes::basic::RED; -/// # use bevy_asset::{Handle, AssetServer, Assets, Asset}; +/// # use bevy_asset::{Handle, AssetServer, AssetCommands, Asset}; /// # use bevy_math::primitives::Capsule3d; /// # /// #[derive(AsBindGroup, Debug, Clone, Asset, TypePath)] @@ -122,13 +122,12 @@ pub const MATERIAL_BIND_GROUP_INDEX: usize = 3; /// // Spawn an entity with a mesh using `CustomMaterial`. /// fn setup( /// mut commands: Commands, -/// mut meshes: ResMut>, -/// mut materials: ResMut>, +/// mut asset_commands: AssetCommands, /// asset_server: Res /// ) { /// commands.spawn(( -/// Mesh3d(meshes.add(Capsule3d::default())), -/// MeshMaterial3d(materials.add(CustomMaterial { +/// Mesh3d(asset_commands.spawn_asset(Capsule3d::default().into())), +/// MeshMaterial3d(asset_commands.spawn_asset(CustomMaterial { /// color: RED.into(), /// color_texture: asset_server.load("some_image.png"), /// })), diff --git a/crates/bevy_pbr/src/mesh_material.rs b/crates/bevy_pbr/src/mesh_material.rs index 934964dda55ef..dea8d297a1306 100644 --- a/crates/bevy_pbr/src/mesh_material.rs +++ b/crates/bevy_pbr/src/mesh_material.rs @@ -18,18 +18,17 @@ use derive_more::derive::From; /// # use bevy_ecs::prelude::*; /// # use bevy_mesh::{Mesh, Mesh3d}; /// # use bevy_color::palettes::basic::RED; -/// # use bevy_asset::Assets; +/// # use bevy_asset::AssetCommands; /// # use bevy_math::primitives::Capsule3d; /// # /// // Spawn an entity with a mesh using `StandardMaterial`. /// fn setup( /// mut commands: Commands, -/// mut meshes: ResMut>, -/// mut materials: ResMut>, +/// mut asset_commands: AssetCommands, /// ) { /// commands.spawn(( -/// Mesh3d(meshes.add(Capsule3d::default())), -/// MeshMaterial3d(materials.add(StandardMaterial { +/// Mesh3d(asset_commands.spawn_asset(Capsule3d::default().into())), +/// MeshMaterial3d(asset_commands.spawn_asset(StandardMaterial { /// base_color: RED.into(), /// ..Default::default() /// })), diff --git a/crates/bevy_pbr/src/meshlet/instance_manager.rs b/crates/bevy_pbr/src/meshlet/instance_manager.rs index 068736b390be6..01df1a4d88dff 100644 --- a/crates/bevy_pbr/src/meshlet/instance_manager.rs +++ b/crates/bevy_pbr/src/meshlet/instance_manager.rs @@ -3,7 +3,7 @@ use crate::{ meshlet::asset::MeshletAabb, MaterialBindingId, MeshFlags, MeshTransforms, MeshUniform, PreviousGlobalTransform, RenderMaterialBindings, RenderMaterialInstances, }; -use bevy_asset::{AssetEvent, AssetServer, Assets, UntypedAssetId}; +use bevy_asset::{AssetCommands, AssetEvent, AssetServer, Assets, UntypedAssetId}; use bevy_camera::visibility::RenderLayers; use bevy_ecs::{ entity::{Entities, Entity, EntityHashMap}, @@ -190,7 +190,7 @@ impl InstanceManager { pub fn extract_meshlet_mesh_entities( mut meshlet_mesh_manager: ResMut, mut instance_manager: ResMut, - // TODO: Replace main_world and system_state when Extract>> is possible + // TODO: Replace main_world and system_state when Extract> is possible mut main_world: ResMut, mesh_material_ids: Res, render_material_bindings: Res, @@ -207,7 +207,8 @@ pub fn extract_meshlet_mesh_entities( Has, )>, Res, - ResMut>, + Assets, + AssetCommands, MessageReader>, )>, >, @@ -219,7 +220,7 @@ pub fn extract_meshlet_mesh_entities( *system_state = Some(SystemState::new(&mut main_world)); } let system_state = system_state.as_mut().unwrap(); - let (instances_query, asset_server, mut assets, mut asset_events) = + let (instances_query, asset_server, assets, mut asset_commands, mut asset_events) = system_state.get_mut(&mut main_world).unwrap(); // Reset per-frame data @@ -253,8 +254,11 @@ pub fn extract_meshlet_mesh_entities( } // Upload the instance's MeshletMesh asset data if not done already done - let (root_bvh_node, aabb, bvh_depth) = - meshlet_mesh_manager.queue_upload_if_needed(meshlet_mesh.id(), &mut assets); + let (root_bvh_node, aabb, bvh_depth) = meshlet_mesh_manager.queue_upload_if_needed( + meshlet_mesh.id(), + &assets, + &mut asset_commands, + ); // Add the instance's data to the instance manager instance_manager.add_instance( diff --git a/crates/bevy_pbr/src/meshlet/meshlet_mesh_manager.rs b/crates/bevy_pbr/src/meshlet/meshlet_mesh_manager.rs index 29d7a75d57fd3..d547fd6bd6974 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::{AssetCommands, AssetId, Assets}; use bevy_ecs::{ resource::Resource, system::{Commands, Res, ResMut}, @@ -47,12 +47,15 @@ impl MeshletMeshManager { pub fn queue_upload_if_needed( &mut self, asset_id: AssetId, - assets: &mut Assets, + assets: &Assets, + asset_commands: &mut AssetCommands, ) -> (u32, MeshletAabb, u32) { let queue_meshlet_mesh = |asset_id: &AssetId| { - let meshlet_mesh = assets.remove_untracked(*asset_id).expect( + let meshlet_mesh = assets.get(*asset_id).expect( "MeshletMesh asset was already unloaded but is not registered with MeshletMeshManager", ); + // Remove the asset since we're uploading it now. + asset_commands.remove_asset(*asset_id); let vertex_positions_slice = self .vertex_positions diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 6a90c9793f4f1..b3b6c95328da1 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -6,7 +6,7 @@ use crate::{ }; use alloc::sync::Arc; use bevy_asset::uuid::Uuid; -use bevy_asset::{embedded_asset, load_embedded_asset, AssetId, AssetIndex, AssetServer}; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetEntity, AssetId, AssetServer}; use bevy_camera::visibility::NoCpuCulling; use bevy_camera::{ primitives::Aabb, @@ -932,8 +932,8 @@ impl From> for MeshAssetIdFlat { #[inline] fn from(value: AssetId) -> Self { match value { - AssetId::Index { index, .. } => { - let bits = index.to_bits(); + AssetId::Entity { entity, .. } => { + let bits = entity.raw_entity().to_bits(); MeshAssetIdFlat { mode: MESH_ASSET_ID_FLAT_MODE_INDEX, words: [(bits & 0xffff_ffff) as u32, (bits >> 32) as u32, 0, 0], @@ -959,9 +959,9 @@ impl From for AssetId { #[inline] fn from(value: MeshAssetIdFlat) -> AssetId { if value.mode == MESH_ASSET_ID_FLAT_MODE_INDEX { - AssetId::from(AssetIndex::from_bits( + AssetId::from(AssetEntity::new_unchecked(Entity::from_bits( (value.words[0] as u64) | ((value.words[1] as u64) << 32), - )) + ))) } else { let lo = (value.words[0] as u64) | ((value.words[1] as u64) << 32); let hi = (value.words[2] as u64) | ((value.words[3] as u64) << 32); diff --git a/crates/bevy_pbr/src/render/skin.rs b/crates/bevy_pbr/src/render/skin.rs index c9ab356ab9ab0..8ab7bcb3a31c2 100644 --- a/crates/bevy_pbr/src/render/skin.rs +++ b/crates/bevy_pbr/src/render/skin.rs @@ -282,7 +282,7 @@ pub fn extract_skins( )>, >, >, - skinned_mesh_inverse_bindposes: Extract>>, + skinned_mesh_inverse_bindposes: Extract>, changed_transforms: Extract>>, joints: Extract>, mut removed_skinned_meshes_query: Extract>, diff --git a/crates/bevy_pbr/src/volumetric_fog/mod.rs b/crates/bevy_pbr/src/volumetric_fog/mod.rs index 661032dd2aab3..6fe3c8e8456dc 100644 --- a/crates/bevy_pbr/src/volumetric_fog/mod.rs +++ b/crates/bevy_pbr/src/volumetric_fog/mod.rs @@ -30,7 +30,7 @@ //! [Henyey-Greenstein phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions#TheHenyeyndashGreensteinPhaseFunction use bevy_app::{App, Plugin}; -use bevy_asset::{embedded_asset, Assets, Handle}; +use bevy_asset::{embedded_asset, DirectAssetAccessExt, Handle}; use bevy_core_pipeline::{ core_3d::prepare_core_3d_depth_textures, schedule::{Core3d, Core3dSystems}, @@ -66,9 +66,12 @@ impl Plugin for VolumetricFogPlugin { fn build(&self, app: &mut App) { embedded_asset!(app, "volumetric_fog.wgsl"); - let mut meshes = app.world_mut().resource_mut::>(); - let plane_mesh = meshes.add(Plane3d::new(Vec3::Z, Vec2::ONE).mesh()); - let cube_mesh = meshes.add(Cuboid::new(1.0, 1.0, 1.0).mesh()); + let plane_mesh = app + .world_mut() + .spawn_asset(Mesh::from(Plane3d::new(Vec3::Z, Vec2::ONE).mesh())); + let cube_mesh = app + .world_mut() + .spawn_asset(Mesh::from(Cuboid::new(1.0, 1.0, 1.0).mesh())); app.add_plugins(SyncComponentPlugin::::default()); diff --git a/crates/bevy_pbr/src/wireframe.rs b/crates/bevy_pbr/src/wireframe.rs index 48df9fa16bd39..9e67e4f32b089 100644 --- a/crates/bevy_pbr/src/wireframe.rs +++ b/crates/bevy_pbr/src/wireframe.rs @@ -8,7 +8,7 @@ use crate::{ use bevy_app::{App, Plugin, PostUpdate, Startup}; use bevy_asset::{ embedded_asset, load_embedded_asset, prelude::AssetChanged, AsAssetId, Asset, AssetApp, - AssetEventSystems, AssetId, AssetServer, Assets, Handle, UntypedAssetId, + AssetCommands, AssetEventSystems, AssetId, AssetServer, AssetsMut, Handle, UntypedAssetId, }; use bevy_camera::{visibility::ViewVisibility, Camera, Camera3d}; use bevy_color::{Color, ColorToComponents}; @@ -1030,11 +1030,11 @@ pub fn extract_wireframe_materials( fn setup_global_wireframe_material( mut commands: Commands, - mut materials: ResMut>, + mut asset_commands: AssetCommands, config: Res, ) { commands.insert_resource(GlobalWireframeMaterial { - handle: materials.add(WireframeMaterial { + handle: asset_commands.spawn_asset(WireframeMaterial { color: config.default_color, line_width: config.default_line_width, topology: config.default_topology, @@ -1043,8 +1043,9 @@ fn setup_global_wireframe_material( } fn wireframe_config_changed( + mut asset_commands: AssetCommands, config: Res, - mut materials: ResMut>, + mut materials: AssetsMut, global_material: Res, mut per_entity_wireframes: Query< ( @@ -1066,7 +1067,7 @@ fn wireframe_config_changed( if handle.0 == global_material.handle { continue; } - handle.0 = materials.add(WireframeMaterial { + handle.0 = asset_commands.spawn_asset(WireframeMaterial { color: maybe_color.map(|c| c.color).unwrap_or(config.default_color), line_width: maybe_width .map(|w| w.width) @@ -1077,7 +1078,7 @@ fn wireframe_config_changed( } fn wireframe_color_changed( - mut materials: ResMut>, + mut asset_commands: AssetCommands, mut colors_changed: Query< ( &mut Mesh3dWireframe, @@ -1090,7 +1091,7 @@ fn wireframe_color_changed( config: Res, ) { for (mut handle, wireframe_color, maybe_width, maybe_topology) in &mut colors_changed { - handle.0 = materials.add(WireframeMaterial { + handle.0 = asset_commands.spawn_asset(WireframeMaterial { color: wireframe_color.color, line_width: maybe_width .map(|w| w.width) @@ -1101,7 +1102,7 @@ fn wireframe_color_changed( } fn wireframe_line_width_changed( - mut materials: ResMut>, + mut asset_commands: AssetCommands, mut widths_changed: Query< ( &mut Mesh3dWireframe, @@ -1114,7 +1115,7 @@ fn wireframe_line_width_changed( config: Res, ) { for (mut handle, wireframe_width, maybe_color, maybe_topology) in &mut widths_changed { - handle.0 = materials.add(WireframeMaterial { + handle.0 = asset_commands.spawn_asset(WireframeMaterial { color: maybe_color.map(|c| c.color).unwrap_or(config.default_color), line_width: wireframe_width.width, topology: maybe_topology.copied().unwrap_or(config.default_topology), @@ -1123,7 +1124,7 @@ fn wireframe_line_width_changed( } fn wireframe_topology_changed( - mut materials: ResMut>, + mut asset_commands: AssetCommands, mut topology_changed: Query< ( &mut Mesh3dWireframe, @@ -1136,7 +1137,7 @@ fn wireframe_topology_changed( config: Res, ) { for (mut handle, topology, maybe_color, maybe_width) in &mut topology_changed { - handle.0 = materials.add(WireframeMaterial { + handle.0 = asset_commands.spawn_asset(WireframeMaterial { color: maybe_color.map(|c| c.color).unwrap_or(config.default_color), line_width: maybe_width .map(|w| w.width) @@ -1150,7 +1151,7 @@ fn wireframe_topology_changed( /// for any mesh with a [`NoWireframe`] component. fn apply_wireframe_material( mut commands: Commands, - mut materials: ResMut>, + mut asset_commands: AssetCommands, wireframes: Query< ( Entity, @@ -1177,9 +1178,9 @@ fn apply_wireframe_material( maybe_color, maybe_width, maybe_topology, - &mut materials, &global_material, &config, + &mut asset_commands, ); material_to_spawn.push((e, Mesh3dWireframe(material))); } @@ -1191,6 +1192,7 @@ type WireframeFilter = (With, Without, Without); /// Applies or removes a wireframe material on any mesh without a [`Wireframe`] or [`NoWireframe`] component. fn apply_global_wireframe_material( mut commands: Commands, + mut asset_commands: AssetCommands, config: Res, meshes_without_material: Query< ( @@ -1203,7 +1205,6 @@ fn apply_global_wireframe_material( >, meshes_with_global_material: Query)>, global_material: Res, - mut materials: ResMut>, ) { if config.global { let mut material_to_spawn = vec![]; @@ -1212,9 +1213,9 @@ fn apply_global_wireframe_material( maybe_color, maybe_width, maybe_topology, - &mut materials, &global_material, &config, + &mut asset_commands, ); // We only add the material handle but not the Wireframe component // This makes it easy to detect which mesh is using the global material and which ones are user specified @@ -1233,12 +1234,12 @@ fn get_wireframe_material( maybe_color: Option<&WireframeColor>, maybe_width: Option<&WireframeLineWidth>, maybe_topology: Option<&WireframeTopology>, - wireframe_materials: &mut Assets, global_material: &GlobalWireframeMaterial, config: &WireframeConfig, + asset_commands: &mut AssetCommands, ) -> Handle { if maybe_color.is_some() || maybe_width.is_some() || maybe_topology.is_some() { - wireframe_materials.add(WireframeMaterial { + asset_commands.spawn_asset(WireframeMaterial { color: maybe_color.map(|c| c.color).unwrap_or(config.default_color), line_width: maybe_width .map(|w| w.width) diff --git a/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs b/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs index 057537ea57a39..7bde4f8a42c43 100644 --- a/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs +++ b/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs @@ -172,7 +172,7 @@ type MeshFilter = Or<(With, With, With)>; #[derive(SystemParam)] pub struct MeshRayCast<'w, 's> { #[doc(hidden)] - pub meshes: Res<'w, Assets>, + pub meshes: Assets<'w, 's, Mesh>, #[doc(hidden)] pub hits: Local<'s, Vec<(FloatOrd, (Entity, RayMeshHit))>>, #[doc(hidden)] 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 8e60bbb2b0ce8..44df26fe4f66e 100644 --- a/crates/bevy_post_process/src/auto_exposure/compensation_curve.rs +++ b/crates/bevy_post_process/src/auto_exposure/compensation_curve.rs @@ -1,4 +1,4 @@ -use bevy_asset::{prelude::*, RenderAssetUsages}; +use bevy_asset::{prelude::*, AssetId, RenderAssetUsages}; use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem}; use bevy_math::{cubic_splines::CubicGenerator, FloatExt, Vec2}; use bevy_reflect::prelude::*; @@ -83,12 +83,15 @@ impl AutoExposureCompensationCurve { /// # Example /// /// ``` - /// # use bevy_asset::prelude::*; + /// # use bevy_app::App; + /// # use bevy_asset::{prelude::*, MinimalAssetPlugin}; /// # use bevy_math::vec2; /// # use bevy_math::cubic_splines::*; /// # use bevy_post_process::auto_exposure::AutoExposureCompensationCurve; - /// # let mut compensation_curves = Assets::::default(); - /// let curve: Handle = compensation_curves.add( + /// # let mut app = App::new(); + /// # app.add_plugins(MinimalAssetPlugin); + /// # let mut asset_commands = app.world_mut().asset_commands(); + /// let curve: Handle = asset_commands.spawn_asset( /// AutoExposureCompensationCurve::from_curve(LinearSpline::new([ /// vec2(-4.0, -2.0), /// vec2(0.0, 0.0), diff --git a/crates/bevy_post_process/src/auto_exposure/mod.rs b/crates/bevy_post_process/src/auto_exposure/mod.rs index 49f245f148670..c6f49ea9a660c 100644 --- a/crates/bevy_post_process/src/auto_exposure/mod.rs +++ b/crates/bevy_post_process/src/auto_exposure/mod.rs @@ -1,5 +1,5 @@ use bevy_app::prelude::*; -use bevy_asset::{embedded_asset, AssetApp, Assets, Handle}; +use bevy_asset::{embedded_asset, AssetApp, AssetId, DirectAssetAccessExt}; use bevy_ecs::prelude::*; use bevy_render::{ diagnostic::RecordDiagnostics, @@ -52,9 +52,10 @@ impl Plugin for AutoExposurePlugin { .init_asset::() .register_asset_reflect::(); app.world_mut() - .resource_mut::>() - .insert(&Handle::default(), AutoExposureCompensationCurve::default()) - .unwrap(); + .spawn_uuid_asset::( + AssetId::<()>::DEFAULT_UUID, + AutoExposureCompensationCurve::default(), + ); app.add_plugins(ExtractComponentPlugin::::default()); diff --git a/crates/bevy_post_process/src/effect_stack/mod.rs b/crates/bevy_post_process/src/effect_stack/mod.rs index 7c847d8ddf2d0..5471cae5e1ba9 100644 --- a/crates/bevy_post_process/src/effect_stack/mod.rs +++ b/crates/bevy_post_process/src/effect_stack/mod.rs @@ -21,7 +21,8 @@ use crate::effect_stack::chromatic_aberration::{ use bevy_app::{App, Plugin}; use bevy_asset::{ - embedded_asset, load_embedded_asset, AssetServer, Assets, Handle, RenderAssetUsages, + embedded_asset, load_embedded_asset, AssetServer, DirectAssetAccessExt, Handle, + RenderAssetUsages, }; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ @@ -131,8 +132,7 @@ impl Plugin for EffectStackPlugin { embedded_asset!(app, "post_process.wgsl"); // Load the default chromatic aberration LUT. - let mut assets = app.world_mut().resource_mut::>(); - let default_lut = assets.add(Image::new( + let default_lut = app.world_mut().spawn_asset(Image::new( Extent3d { width: 3, height: 1, diff --git a/crates/bevy_render/src/camera.rs b/crates/bevy_render/src/camera.rs index 29d15a00b0b97..85f93754820b5 100644 --- a/crates/bevy_render/src/camera.rs +++ b/crates/bevy_render/src/camera.rs @@ -343,7 +343,7 @@ pub enum MissingRenderTargetInfoError { /// /// ## World Resources /// -/// [`Res>`](Assets) -- For cameras that render to an image, this resource is used to +/// [`Assets`](Assets) -- For cameras that render to an image, this resource is used to /// inspect information about the render target. This system will not access any other image assets. /// /// [`OrthographicProjection`]: bevy_camera::OrthographicProjection @@ -355,7 +355,7 @@ pub fn camera_system( mut image_asset_event_reader: MessageReader>, primary_window: Query>, windows: Query<(Entity, &Window)>, - images: Res>, + images: Assets, manual_texture_views: Res, mut cameras: Query<(&mut Camera, &RenderTarget, &mut Projection)>, ) -> Result<(), BevyError> { diff --git a/crates/bevy_render/src/erased_render_asset.rs b/crates/bevy_render/src/erased_render_asset.rs index eeba62d5ba519..1210a3f1f18ae 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::{ RenderStartup, 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, DirectAssetAccessExt, UntypedAssetId}; +use bevy_asset::{AssetsMut, RenderAssetUsages}; use bevy_ecs::{ prelude::{Commands, IntoScheduleConfigs, Local, MessageReader, ResMut, Resource}, schedule::{ScheduleConfigs, SystemSet}, @@ -232,7 +232,7 @@ impl ErasedRenderAssets { struct CachedExtractErasedRenderAssetSystemState { state: SystemState<( MessageReader<'static, 'static, AssetEvent>, - ResMut<'static, Assets>, + AssetsMut<'static, 'static, A::SourceAsset>, )>, } @@ -295,8 +295,8 @@ pub(crate) fn extract_erased_render_asset( .filter(|ids| !ids.is_empty()); main_world.resource_scope( - |world, mut cached_state: Mut>| { - let (mut events, mut assets) = cached_state.state.get_mut(world).unwrap(); + |main_world, mut cached_state: Mut>| { + let (mut events, assets) = cached_state.state.get_mut(main_world).unwrap(); if let Some(reextract_ids) = reextract_ids { needs_extracting.extend(reextract_ids); @@ -330,15 +330,13 @@ pub(crate) fn extract_erased_render_asset( } } + let mut take_later = >::default(); for id in needs_extracting.drain() { if let Some(asset) = assets.get(id) { let asset_usage = A::asset_usage(asset); if asset_usage.contains(RenderAssetUsages::RENDER_WORLD) { if asset_usage == RenderAssetUsages::RENDER_WORLD { - if let Some(asset) = assets.remove(id) { - extracted_assets.extracted.push((id, asset)); - extracted_assets.added.insert(id); - } + take_later.insert(id); } else { extracted_assets.extracted.push((id, asset.clone())); extracted_assets.added.insert(id); @@ -346,8 +344,16 @@ pub(crate) fn extract_erased_render_asset( } } } - - cached_state.state.apply(world); + cached_state.state.apply(main_world); + + for id in take_later { + let Ok(asset) = main_world.remove_asset(id) else { + // Ignore assets we've previously removed. + continue; + }; + extracted_assets.extracted.push((id, asset)); + extracted_assets.added.insert(id); + } }, ); } diff --git a/crates/bevy_render/src/mesh/mod.rs b/crates/bevy_render/src/mesh/mod.rs index c176b52aee731..bb072bbfe16dc 100644 --- a/crates/bevy_render/src/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mod.rs @@ -14,7 +14,7 @@ use crate::{ }; use allocator::MeshAllocatorPlugin; use bevy_app::{App, Plugin}; -use bevy_asset::{AssetId, Assets, Handle, RenderAssetUsages}; +use bevy_asset::{AssetId, DirectAssetAccessExt, Handle, RenderAssetUsages}; use bevy_camera::primitives::MeshAabb; use bevy_ecs::{ prelude::*, @@ -61,8 +61,7 @@ impl Plugin for MeshRenderAssetPlugin { } fn finish(&self, app: &mut App) { - let mut mesh_assets = app.world_mut().resource_mut::>(); - let handle = mesh_assets.add( + let handle = app.world_mut().spawn_asset( Mesh::new(PrimitiveTopology::PointList, RenderAssetUsages::all()) .with_inserted_attribute( Mesh::ATTRIBUTE_POSITION, diff --git a/crates/bevy_render/src/render_asset.rs b/crates/bevy_render/src/render_asset.rs index a3ea3613d1c76..22746f079f7a5 100644 --- a/crates/bevy_render/src/render_asset.rs +++ b/crates/bevy_render/src/render_asset.rs @@ -3,8 +3,9 @@ use crate::{ RenderStartup, RenderSystems, Res, }; use bevy_app::{App, Plugin, SubApp}; -use bevy_asset::{Asset, AssetEvent, AssetId, Assets, RenderAssetUsages}; +use bevy_asset::{Asset, AssetEvent, AssetId, AssetsMut, RenderAssetUsages}; use bevy_ecs::{ + change_detection::DetectChangesMut, prelude::{Commands, IntoScheduleConfigs, Local, MessageReader, ResMut, Resource}, schedule::{ScheduleConfigs, SystemSet}, system::{ScheduleSystem, StaticSystemParam, SystemParam, SystemParamItem, SystemState}, @@ -242,7 +243,7 @@ impl RenderAssets { struct CachedExtractRenderAssetSystemState { state: SystemState<( MessageReader<'static, 'static, AssetEvent>, - ResMut<'static, Assets>, + AssetsMut<'static, 'static, A::SourceAsset>, Option>>, )>, } @@ -335,9 +336,9 @@ pub(crate) fn extract_render_asset( let asset_usage = A::asset_usage(asset); if asset_usage.contains(RenderAssetUsages::RENDER_WORLD) { if asset_usage == RenderAssetUsages::RENDER_WORLD { - if let Some(asset) = assets.get_mut_untracked(id) { + if let Some(mut asset) = assets.get_mut(id) { let previous_asset = maybe_render_assets.as_ref().and_then(|render_assets| render_assets.get(id)); - match A::take_gpu_data(asset, previous_asset) { + match A::take_gpu_data(asset.bypass_change_detection(), previous_asset) { Ok(gpu_data_asset) => { extracted_assets.extracted.push((id, gpu_data_asset)); extracted_assets.added.insert(id); diff --git a/crates/bevy_render/src/render_resource/pipeline_cache.rs b/crates/bevy_render/src/render_resource/pipeline_cache.rs index 6b98633646a61..c7648ee79c088 100644 --- a/crates/bevy_render/src/render_resource/pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/pipeline_cache.rs @@ -11,11 +11,7 @@ use crate::{ }; use alloc::{borrow::Cow, sync::Arc}; use bevy_asset::{AssetEvent, AssetId, Assets, Handle}; -use bevy_ecs::{ - message::MessageReader, - resource::Resource, - system::{Res, ResMut}, -}; +use bevy_ecs::{message::MessageReader, resource::Resource, system::ResMut}; use bevy_log::error; use bevy_platform::collections::{hash_map::RawEntryMut, HashMap, HashSet}; use bevy_shader::{ @@ -746,7 +742,7 @@ impl PipelineCache { pub(crate) fn extract_shaders( mut cache: ResMut, - shaders: Extract>>, + shaders: Extract>, mut events: Extract>>, ) { if cache.needs_shader_reload { @@ -754,7 +750,7 @@ impl PipelineCache { for (id, shader) in shaders.iter() { let mut shader = shader.clone(); shader.shader_defs.extend(cache.global_shader_defs.clone()); - cache.set_shader(id, shader); + cache.set_shader(id.into(), shader); } // Drain events so we don't double-process shaders we just loaded. for _ in events.read() {} diff --git a/crates/bevy_scene/src/lib.rs b/crates/bevy_scene/src/lib.rs index 25f2642fecc14..d624f29408b72 100644 --- a/crates/bevy_scene/src/lib.rs +++ b/crates/bevy_scene/src/lib.rs @@ -1092,7 +1092,9 @@ mod tests { use bevy_app::{App, TaskPoolPlugin}; use bevy_asset::io::memory::{Dir, MemoryAssetReader}; use bevy_asset::io::{AssetSourceBuilder, AssetSourceId}; - use bevy_asset::{Asset, AssetApp, AssetLoader, AssetPlugin, AssetServer, Assets, Handle}; + use bevy_asset::{ + Asset, AssetApp, AssetLoader, AssetPlugin, AssetServer, DirectAssetAccessExt, Handle, + }; use bevy_ecs::lifecycle::HookContext; use bevy_ecs::name::Name; use bevy_ecs::prelude::*; @@ -1268,14 +1270,9 @@ mod tests { // Insert an asset that the fake loader can fake read. dir.insert_asset_text(Path::new("a.bsn"), ""); let asset_server = app.world().resource::().clone(); - let handle = asset_server.load("a.bsn"); - assert!(app.world().get_resource::>().is_some()); + let handle = asset_server.load::("a.bsn"); run_app_until(&mut app, || asset_server.is_loaded(&handle)); - let patch = app - .world() - .resource::>() - .get(&handle) - .unwrap(); + let patch = app.world().get_asset(handle.id()).unwrap(); assert!(patch.resolved.is_some()); let world = app.world_mut(); diff --git a/crates/bevy_scene/src/resolved_scene.rs b/crates/bevy_scene/src/resolved_scene.rs index 6d451fb5ef703..66abcaf3b6e20 100644 --- a/crates/bevy_scene/src/resolved_scene.rs +++ b/crates/bevy_scene/src/resolved_scene.rs @@ -6,6 +6,7 @@ use bevy_ecs::{ entity::Entity, error::{BevyError, Result}, relationship::{Relationship, RelationshipTarget}, + system::SystemState, template::{SceneEntityReference, SceneEntityReferences, Template, TemplateContext}, world::{EntityWorldMut, World}, }; @@ -232,7 +233,10 @@ impl ResolvedScene { .set(entity_reference, context.entity.id()); } if let Some(cached) = &self.cached { - let scene_patches = context.resource::>(); + let mut state = context + .entity + .world_scope(|world| SystemState::>::new(world)); + let scene_patches = state.get(context.entity.world()).unwrap(); let Some(patch) = scene_patches.get(&cached.handle) else { return Err(ApplySceneError::MissingCachedScene { path: cached.handle.path().cloned(), diff --git a/crates/bevy_scene/src/scene.rs b/crates/bevy_scene/src/scene.rs index 3e935344ef639..afeb5539ecb8d 100644 --- a/crates/bevy_scene/src/scene.rs +++ b/crates/bevy_scene/src/scene.rs @@ -167,11 +167,11 @@ pub enum ResolveSceneError { } /// Context used by [`Scene`] implementations during [`Scene::resolve`]. -pub struct ResolveContext<'a> { +pub struct ResolveContext<'a, 'w, 's> { /// The current asset server pub assets: &'a AssetServer, /// The current [`ScenePatch`] asset collection - pub patches: &'a Assets, + pub patches: &'a Assets<'w, 's, ScenePatch>, /// The currently cached [`ScenePatch`], if there is one. pub cached: Option<&'a ScenePatch>, } diff --git a/crates/bevy_scene/src/scene_patch.rs b/crates/bevy_scene/src/scene_patch.rs index 549ce733445a2..02fc01940cfb3 100644 --- a/crates/bevy_scene/src/scene_patch.rs +++ b/crates/bevy_scene/src/scene_patch.rs @@ -125,6 +125,17 @@ pub struct SceneListPatch { } impl SceneListPatch { + /// Returns an instance that doesn't do anything and is fairly cheap. + /// + /// This is good for use with [`core::mem::replace`]. + pub(crate) fn dummy() -> Self { + Self { + scene_list: None, + dependencies: vec![], + resolved: None, + } + } + /// Kicks off a load of the `scene_list`. This enumerates the scene list's dependencies using [`SceneList::register_dependencies`], loads /// them using the given [`AssetServer`], and assigns the resulting asset handles to [`SceneListPatch::dependencies`]. pub fn load(assets: &AssetServer, scene_list: L) -> Self { diff --git a/crates/bevy_scene/src/spawn.rs b/crates/bevy_scene/src/spawn.rs index eac85b446d383..75e17788f7725 100644 --- a/crates/bevy_scene/src/spawn.rs +++ b/crates/bevy_scene/src/spawn.rs @@ -3,9 +3,10 @@ use crate::{ SpawnSceneError, }; use alloc::sync::Arc; -use bevy_asset::{AssetEvent, AssetServer, Assets, Handle}; +use bevy_asset::{AssetEvent, AssetServer, Assets, AssetsMut, DirectAssetAccessExt, Handle}; use bevy_ecs::{ bundle::BundleScratch, message::MessageCursor, prelude::*, relationship::Relationship, + system::SystemState, }; use bevy_platform::collections::HashMap; use tracing::error; @@ -186,9 +187,10 @@ pub trait WorldSceneExt { impl WorldSceneExt for World { fn spawn_scene(&mut self, scene: S) -> Result, SpawnSceneError> { + let mut state = SystemState::>::new(self); let assets = self.resource::(); let mut patch = ScenePatch::load(assets, scene); - patch.resolve(assets, self.resource::>())?; + patch.resolve(assets, &state.get(self).unwrap())?; patch.spawn(self) } @@ -209,9 +211,10 @@ impl WorldSceneExt for World { &mut self, scenes: L, ) -> Result, SpawnSceneError> { + let mut state = SystemState::>::new(self); let assets = self.resource::(); let mut patch = SceneListPatch::load(assets, scenes); - patch.resolve(assets, self.resource::>())?; + patch.resolve(assets, &state.get(self).unwrap())?; patch.spawn(self) } @@ -497,9 +500,10 @@ impl EntityWorldMutSceneExt for EntityWorldMut<'_> { } fn apply_scene(&mut self, scene: S) -> Result<(), SpawnSceneError> { + let mut state = self.world_scope(|world| SystemState::>::new(world)); let assets = self.resource::(); let mut patch = ScenePatch::load(assets, scene); - patch.resolve(assets, self.resource::>())?; + patch.resolve(assets, &state.get(self.world()).unwrap())?; patch.apply(self) } @@ -604,15 +608,15 @@ pub fn resolve_scene_patches( mut events: MessageReader>, mut list_events: MessageReader>, assets: Res, - mut patches: ResMut>, - mut list_patches: ResMut>, + mut patches: AssetsMut, + mut list_patches: AssetsMut, mut waiting: ResMut, ) { for event in events.read() { match *event { AssetEvent::LoadedWithDependencies { id } => { if let Some(scene) = patches.get_mut(id).and_then(|mut p| p.scene.take()) { - match ResolvedSceneRoot::resolve(scene, &assets, &patches) { + match ResolvedSceneRoot::resolve(scene, &assets, &patches.as_readonly()) { Ok(resolved) => { let mut patch = patches.get_mut(id).unwrap(); patch.resolved = Some(Arc::new(resolved)); @@ -637,7 +641,7 @@ pub fn resolve_scene_patches( match *event { AssetEvent::LoadedWithDependencies { id } => { if let Some(mut list_patch) = list_patches.get_mut(id) - && let Err(err) = list_patch.resolve(&assets, &patches) + && let Err(err) = list_patch.resolve(&assets, &patches.as_readonly()) { error!("Failed to resolve scene list {id}: {err}"); } @@ -704,82 +708,102 @@ pub fn on_add_scene_patch_instance( pub fn spawn_queued( world: &mut World, scene_patch_instances: &mut QueryState<&ScenePatchInstance>, + patches: &mut SystemState>, + list_patches: &mut SystemState>, mut queued: Local, mut bundle_scratch: Local, mut reader: Local>>, mut list_reader: Local>>, ) { - world.resource_scope(|world, mut list_patches: Mut>| { - world.resource_scope(|world, mut waiting: Mut| { - world.resource_scope(|world, events: Mut>>| { - for event in reader.read(&events) { - let patches = world.resource::>(); - if let AssetEvent::LoadedWithDependencies { id } = event - && let Some(resolved) = patches.get(*id).and_then(|p| p.resolved.clone()) - && let Some(entities) = waiting.scene_entities.remove(id) - { - for entity in entities { - if let Ok(mut entity_mut) = world.get_entity_mut(entity) - && let Err(err) = - resolved.apply(&mut entity_mut, &mut bundle_scratch) - { - error!( - "Failed to apply scene (id: {}) to entity {entity}: {}", - id, err - ); - } + world.resource_scope(|world, mut waiting: Mut| { + world.resource_scope(|world, events: Mut>>| { + for event in reader.read(&events) { + let patches = patches.get(world).unwrap(); + if let AssetEvent::LoadedWithDependencies { id } = event + && let Some(resolved) = patches.get(*id).and_then(|p| p.resolved.clone()) + && let Some(entities) = waiting.scene_entities.remove(id) + { + for entity in entities { + if let Ok(mut entity_mut) = world.get_entity_mut(entity) + && let Err(err) = resolved.apply(&mut entity_mut, &mut bundle_scratch) + { + error!( + "Failed to apply scene (id: {}) to entity {entity}: {}", + id, err + ); } } } - }); - world.resource_scope( - |world, list_events: Mut>>| { - for event in list_reader.read(&list_events) { - if let AssetEvent::LoadedWithDependencies { id } = event - && let Some(list_patch) = list_patches.get_mut(*id) - { - if let Some(scene_list_spawns) = - waiting.related_list_entities.remove(id) - { - for scene_list_spawn in scene_list_spawns { - let result = list_patch.spawn_with(world, |entity| { - (scene_list_spawn.insert)(entity, scene_list_spawn.entity); - }); - - if let Err(err) = result { - error!("Failed to spawn scene list (id: {}): {}", id, err); - } + } + }); + world.resource_scope( + |world, list_events: Mut>>| { + for event in list_reader.read(&list_events) { + if let AssetEvent::LoadedWithDependencies { id } = event + && let Some(list_patch) = list_patches.get_mut(world).unwrap().get_mut(*id) + { + let related_list_entities = waiting.related_list_entities.remove(id); + let scene_list_spawns = waiting.scene_list_spawns.remove(id); + + // Avoid the hokey-pokey unless we actually need to spawn. + if related_list_entities.is_none() && scene_list_spawns.is_none() { + continue; + } + + // Hokey-pokey the SceneListPatch out of the world so we can use the + // world mutable borrow. **IMPORTANT**: do not continue or break out of + // this loop, or the SceneListPatch will be dropped! + let list_patch = core::mem::replace( + { list_patch }.bypass_change_detection(), + SceneListPatch::dummy(), + ); + + if let Some(scene_list_spawns) = related_list_entities { + for scene_list_spawn in scene_list_spawns { + let result = list_patch.spawn_with(world, |entity| { + (scene_list_spawn.insert)(entity, scene_list_spawn.entity); + }); + + if let Err(err) = result { + error!("Failed to spawn scene list (id: {}): {}", id, err); } } + } - if let Some(waiting_list_spawns) = waiting.scene_list_spawns.remove(id) - { - for _ in 0..waiting_list_spawns { - let result = list_patch.spawn(world); - if let Err(err) = result { - error!("Failed to spawn scene list (id: {}): {}", id, err); - } + if let Some(waiting_list_spawns) = scene_list_spawns { + for _ in 0..waiting_list_spawns { + let result = list_patch.spawn(world); + if let Err(err) = result { + error!("Failed to spawn scene list (id: {}): {}", id, err); } } } - } - }, - ); - loop { - core::mem::swap(&mut *world.resource_mut::(), &mut queued); - if queued.is_empty() { - break; + // Put the asset back into the world. + *list_patches + .get_mut(world) + .unwrap() + .get_mut(*id) + .unwrap() + .bypass_change_detection() = list_patch; + } } - queued.spawn_queued( - world, - &mut waiting, - scene_patch_instances, - &mut bundle_scratch, - &list_patches, - ); + }, + ); + + loop { + core::mem::swap(&mut *world.resource_mut::(), &mut queued); + if queued.is_empty() { + break; } - }); + queued.spawn_queued( + world, + &mut waiting, + scene_patch_instances, + &mut bundle_scratch, + list_patches, + ); + } }); } @@ -796,11 +820,13 @@ impl QueuedScenes { waiting_scenes: &mut WaitingScenes, scene_patch_instances: &mut QueryState<&ScenePatchInstance>, bundle_scratch: &mut BundleScratch, - list_patches: &Assets, + list_patches: &mut SystemState>, ) { for (entity, handle) in core::mem::take(&mut self.new_scene_entities) { - let patches = world.resource::>(); - if let Some(resolved) = patches.get(&handle).and_then(|p| p.resolved.clone()) { + if let Some(resolved) = world + .get_asset(handle.id()) + .and_then(|p| p.resolved.clone()) + { let mut entity_mut = world.get_entity_mut(entity).unwrap(); if let Err(err) = resolved.apply(&mut entity_mut, bundle_scratch) { let scene_patch_instance = scene_patch_instances.get(world, entity).unwrap(); @@ -822,10 +848,24 @@ impl QueuedScenes { } for (scene_list_spawn, handle) in core::mem::take(&mut self.related_scene_list_spawns) { - if let Some(list_patch) = list_patches.get(&handle) { + if let Some(mut list_patch) = list_patches.get_mut(world).unwrap().get_mut(&handle) { + // Hokey-pokey the SceneListPatch out of the world so we can use the world mutable + // borrow. + let list_patch = core::mem::replace( + // Consume the list_patch, then get a mutable borrow to its value to replace it. + { list_patch }.bypass_change_detection(), + SceneListPatch::dummy(), + ); let result = list_patch.spawn_with(world, |entity| { (scene_list_spawn.insert)(entity, scene_list_spawn.entity); }); + // Put the asset back into the world. + *list_patches + .get_mut(world) + .unwrap() + .get_mut(&handle) + .unwrap() + .bypass_change_detection() = list_patch; if let Err(err) = result { error!( @@ -845,8 +885,20 @@ impl QueuedScenes { } for handle in core::mem::take(&mut self.scene_list_spawns) { - if let Some(list_patch) = list_patches.get(&handle) { + if let Some(mut list_patch) = list_patches.get_mut(world).unwrap().get_mut(&handle) { + let list_patch = core::mem::replace( + { list_patch }.bypass_change_detection(), + SceneListPatch::dummy(), + ); let result = list_patch.spawn(world); + // Put the asset back into the world. + *list_patches + .get_mut(world) + .unwrap() + .get_mut(&handle) + .unwrap() + .bypass_change_detection() = list_patch; + if let Err(err) = result { error!( "Failed to spawn scene list (id: {}, path: {:?}): {}", diff --git a/crates/bevy_solari/src/scene/extract.rs b/crates/bevy_solari/src/scene/extract.rs index deb59e804317a..5dc944795123d 100644 --- a/crates/bevy_solari/src/scene/extract.rs +++ b/crates/bevy_solari/src/scene/extract.rs @@ -1,13 +1,18 @@ +use core::marker::PhantomData; + use super::RaytracingMesh3d; -use bevy_asset::{AssetId, Assets}; +use bevy_asset::{AssetData, AssetEntity, AssetId}; use bevy_derive::Deref; use bevy_ecs::{ + entity::Entity, + lifecycle::RemovedComponents, + query::Changed, resource::Resource, - system::{Commands, Query}, + system::{Commands, Query, ResMut}, }; use bevy_pbr::{MeshMaterial3d, PreviousGlobalTransform, StandardMaterial}; use bevy_platform::collections::HashMap; -use bevy_render::{extract_resource::ExtractResource, sync_world::RenderEntity, Extract}; +use bevy_render::{sync_world::RenderEntity, Extract}; use bevy_transform::components::GlobalTransform; pub fn extract_raytracing_scene( @@ -40,15 +45,28 @@ pub fn extract_raytracing_scene( #[derive(Resource, Deref, Default)] pub struct StandardMaterialAssets(HashMap, StandardMaterial>); -impl ExtractResource for StandardMaterialAssets { - type Source = Assets; - - fn extract_resource(source: &Self::Source) -> Self { - Self( - source - .iter() - .map(|(asset_id, material)| (asset_id, material.clone())) - .collect(), - ) +// TODO: It would be nice for `Assets` to have an API for this instead of us needing to drop down +// into raw queries. +pub fn extract_standard_material_assets( + mut removed_components: Extract>>, + main_world_assets: Extract< + Query<(Entity, &AssetData), Changed>>, + >, + mut extracted_assets: ResMut, +) { + for entity in removed_components.read() { + extracted_assets.0.remove(&AssetId::Entity { + entity: AssetEntity::new_unchecked(entity), + marker: PhantomData, + }); + } + for (entity, asset_data) in main_world_assets.iter() { + extracted_assets.0.insert( + AssetId::Entity { + entity: AssetEntity::new_unchecked(entity), + marker: PhantomData, + }, + asset_data.0.clone(), + ); } } diff --git a/crates/bevy_solari/src/scene/mod.rs b/crates/bevy_solari/src/scene/mod.rs index 2ec11182184cc..75f96086a1c34 100644 --- a/crates/bevy_solari/src/scene/mod.rs +++ b/crates/bevy_solari/src/scene/mod.rs @@ -7,11 +7,10 @@ use bevy_shader::load_shader_library; pub use binder::RaytracingSceneBindings; pub use types::RaytracingMesh3d; -use crate::SolariPlugins; +use crate::{scene::extract::extract_standard_material_assets, SolariPlugins}; use bevy_app::{App, Plugin}; use bevy_ecs::schedule::IntoScheduleConfigs; use bevy_render::{ - extract_resource::ExtractResourcePlugin, mesh::{ allocator::{allocate_and_free_meshes, MeshAllocatorSettings}, RenderMesh, @@ -48,8 +47,6 @@ impl Plugin for RaytracingScenePlugin { return; } - app.add_plugins(ExtractResourcePlugin::::default()); - let render_app = app.sub_app_mut(RenderApp); render_app @@ -61,7 +58,10 @@ impl Plugin for RaytracingScenePlugin { .init_gpu_resource::() .init_gpu_resource::() .insert_resource(RaytracingSceneBindings::new()) - .add_systems(ExtractSchedule, extract_raytracing_scene) + .add_systems( + ExtractSchedule, + (extract_raytracing_scene, extract_standard_material_assets), + ) .add_systems( Render, ( diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 855c05049eef2..9c01f479e1b2d 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -109,9 +109,9 @@ impl Plugin for SpritePlugin { /// Used in system set [`VisibilitySystems::CalculateBounds`]. pub fn calculate_bounds_2d( mut commands: Commands, - meshes: Res>, - images: Res>, - atlases: Res>, + meshes: Assets, + images: Assets, + atlases: Assets, new_mesh_aabb: Query< (Entity, &Mesh2d), ( @@ -214,8 +214,8 @@ pub fn calculate_bounds_2d( // inside the vertex shader which isn't recognized by calculate_aabb(). fn calculate_bounds_2d_sprite_mesh( mut commands: Commands, - images: Res>, - atlases: Res>, + images: Assets, + atlases: Assets, new_sprite_aabb: Query< (Entity, &SpriteMesh, &Anchor), ( @@ -276,6 +276,7 @@ fn calculate_bounds_2d_sprite_mesh( #[cfg(test)] mod test { use super::*; + use bevy_asset::{AssetApp, DirectAssetAccessExt, MinimalAssetPlugin}; use bevy_math::{Rect, Vec2, Vec3A}; #[test] @@ -283,14 +284,12 @@ mod test { // Setup app let mut app = App::new(); + app.add_plugins(MinimalAssetPlugin) + .init_asset::() + .init_asset::(); + // Add resources and get handle to image - let mut image_assets = Assets::::default(); - let image_handle = image_assets.add(Image::default()); - app.insert_resource(image_assets); - let mesh_assets = Assets::::default(); - app.insert_resource(mesh_assets); - let texture_atlas_assets = Assets::::default(); - app.insert_resource(texture_atlas_assets); + let image_handle = app.world_mut().spawn_asset(Image::default()); // Add system app.add_systems(Update, calculate_bounds_2d); @@ -321,14 +320,12 @@ mod test { // Setup app let mut app = App::new(); + app.add_plugins(MinimalAssetPlugin) + .init_asset::() + .init_asset::(); + // Add resources and get handle to image - let mut image_assets = Assets::::default(); - let image_handle = image_assets.add(Image::default()); - app.insert_resource(image_assets); - let mesh_assets = Assets::::default(); - app.insert_resource(mesh_assets); - let texture_atlas_assets = Assets::::default(); - app.insert_resource(texture_atlas_assets); + let image_handle = app.world_mut().spawn_asset(Image::default()); // Add system app.add_systems(Update, calculate_bounds_2d); @@ -384,14 +381,12 @@ mod test { // Setup app let mut app = App::new(); + app.add_plugins(MinimalAssetPlugin) + .init_asset::() + .init_asset::(); + // Add resources and get handle to image - let mut image_assets = Assets::::default(); - let image_handle = image_assets.add(Image::default()); - app.insert_resource(image_assets); - let mesh_assets = Assets::::default(); - app.insert_resource(mesh_assets); - let texture_atlas_assets = Assets::::default(); - app.insert_resource(texture_atlas_assets); + let image_handle = app.world_mut().spawn_asset(Image::default()); // Add system app.add_systems(Update, calculate_bounds_2d); diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index 196abd5df5c22..9a78da0c262d7 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -96,8 +96,8 @@ fn sprite_picking( Option<&RenderLayers>, )>, primary_window: Query>, - images: Res>, - texture_atlas_layout: Res>, + images: Assets, + texture_atlas_layout: Assets, settings: Res, sprite_query: Query<( Entity, diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index f970c9acb1a96..e94311be5eb83 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -295,8 +295,12 @@ impl From for Anchor { #[cfg(test)] mod tests { - use bevy_asset::{Assets, RenderAssetUsages}; + use bevy_app::App; + use bevy_asset::{ + AssetApp, Assets, DirectAssetAccessExt, MinimalAssetPlugin, RenderAssetUsages, + }; use bevy_color::Color; + use bevy_ecs::system::SystemState; use bevy_image::{Image, ToExtents}; use bevy_image::{TextureAtlas, TextureAtlasLayout}; use bevy_math::{Rect, URect, UVec2, Vec2}; @@ -306,6 +310,23 @@ mod tests { use super::Sprite; + fn create_app() -> ( + App, + SystemState<( + Assets<'static, 'static, Image>, + Assets<'static, 'static, TextureAtlasLayout>, + )>, + ) { + let mut app = App::new(); + + app.add_plugins(MinimalAssetPlugin) + .init_asset::() + .init_asset::(); + + let state = SystemState::new(app.world_mut()); + (app, state) + } + /// Makes a new image of the specified size. fn make_image(size: UVec2) -> Image { Image::new_fill( @@ -319,16 +340,15 @@ mod tests { #[test] fn compute_pixel_space_point_for_regular_sprite() { - let mut image_assets = Assets::::default(); - let texture_atlas_assets = Assets::::default(); - - let image = image_assets.add(make_image(UVec2::new(5, 10))); + let (mut app, mut state) = create_app(); + let image = app.world_mut().spawn_asset(make_image(UVec2::new(5, 10))); let sprite = Sprite { image, ..Default::default() }; + let (image_assets, texture_atlas_assets) = state.get(app.world()).unwrap(); let compute = |point| { sprite.compute_pixel_space_point( point, @@ -346,12 +366,12 @@ mod tests { #[test] fn compute_pixel_space_point_for_color_sprite() { - let image_assets = Assets::::default(); - let texture_atlas_assets = Assets::::default(); + let (app, mut state) = create_app(); // This also tests the `custom_size` field. let sprite = Sprite::from_color(Color::BLACK, Vec2::new(50.0, 100.0)); + let (image_assets, texture_atlas_assets) = state.get(app.world()).unwrap(); let compute = |point| { sprite .compute_pixel_space_point( @@ -373,10 +393,9 @@ mod tests { #[test] fn compute_pixel_space_point_for_sprite_with_anchor_bottom_left() { - let mut image_assets = Assets::::default(); - let texture_atlas_assets = Assets::::default(); + let (mut app, mut state) = create_app(); - let image = image_assets.add(make_image(UVec2::new(5, 10))); + let image = app.world_mut().spawn_asset(make_image(UVec2::new(5, 10))); let sprite = Sprite { image, @@ -384,6 +403,7 @@ mod tests { }; let anchor = Anchor::BOTTOM_LEFT; + let (image_assets, texture_atlas_assets) = state.get(app.world()).unwrap(); let compute = |point| { sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets) }; @@ -396,10 +416,9 @@ mod tests { #[test] fn compute_pixel_space_point_for_sprite_with_anchor_top_right() { - let mut image_assets = Assets::::default(); - let texture_atlas_assets = Assets::::default(); + let (mut app, mut state) = create_app(); - let image = image_assets.add(make_image(UVec2::new(5, 10))); + let image = app.world_mut().spawn_asset(make_image(UVec2::new(5, 10))); let sprite = Sprite { image, @@ -407,6 +426,7 @@ mod tests { }; let anchor = Anchor::TOP_RIGHT; + let (image_assets, texture_atlas_assets) = state.get(app.world()).unwrap(); let compute = |point| { sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets) }; @@ -419,10 +439,9 @@ mod tests { #[test] fn compute_pixel_space_point_for_sprite_with_anchor_flip_x() { - let mut image_assets = Assets::::default(); - let texture_atlas_assets = Assets::::default(); + let (mut app, mut state) = create_app(); - let image = image_assets.add(make_image(UVec2::new(5, 10))); + let image = app.world_mut().spawn_asset(make_image(UVec2::new(5, 10))); let sprite = Sprite { image, @@ -431,6 +450,7 @@ mod tests { }; let anchor = Anchor::BOTTOM_LEFT; + let (image_assets, texture_atlas_assets) = state.get(app.world()).unwrap(); let compute = |point| { sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets) }; @@ -443,10 +463,9 @@ mod tests { #[test] fn compute_pixel_space_point_for_sprite_with_anchor_flip_y() { - let mut image_assets = Assets::::default(); - let texture_atlas_assets = Assets::::default(); + let (mut app, mut state) = create_app(); - let image = image_assets.add(make_image(UVec2::new(5, 10))); + let image = app.world_mut().spawn_asset(make_image(UVec2::new(5, 10))); let sprite = Sprite { image, @@ -455,6 +474,7 @@ mod tests { }; let anchor = Anchor::TOP_RIGHT; + let (image_assets, texture_atlas_assets) = state.get(app.world()).unwrap(); let compute = |point| { sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets) }; @@ -467,10 +487,9 @@ mod tests { #[test] fn compute_pixel_space_point_for_sprite_with_rect() { - let mut image_assets = Assets::::default(); - let texture_atlas_assets = Assets::::default(); + let (mut app, mut state) = create_app(); - let image = image_assets.add(make_image(UVec2::new(5, 10))); + let image = app.world_mut().spawn_asset(make_image(UVec2::new(5, 10))); let sprite = Sprite { image, @@ -479,6 +498,7 @@ mod tests { }; let anchor = Anchor::BOTTOM_LEFT; + let (image_assets, texture_atlas_assets) = state.get(app.world()).unwrap(); let compute = |point| { sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets) }; @@ -489,11 +509,10 @@ mod tests { #[test] fn compute_pixel_space_point_for_texture_atlas_sprite() { - let mut image_assets = Assets::::default(); - let mut texture_atlas_assets = Assets::::default(); + let (mut app, mut state) = create_app(); - let image = image_assets.add(make_image(UVec2::new(5, 10))); - let texture_atlas = texture_atlas_assets.add(TextureAtlasLayout { + let image = app.world_mut().spawn_asset(make_image(UVec2::new(5, 10))); + let texture_atlas = app.world_mut().spawn_asset(TextureAtlasLayout { size: UVec2::new(5, 10), textures: vec![URect::new(1, 1, 4, 4)], }); @@ -508,6 +527,7 @@ mod tests { }; let anchor = Anchor::BOTTOM_LEFT; + let (image_assets, texture_atlas_assets) = state.get(app.world()).unwrap(); let compute = |point| { sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets) }; @@ -518,11 +538,10 @@ mod tests { #[test] fn compute_pixel_space_point_for_texture_atlas_sprite_with_rect() { - let mut image_assets = Assets::::default(); - let mut texture_atlas_assets = Assets::::default(); + let (mut app, mut state) = create_app(); - let image = image_assets.add(make_image(UVec2::new(5, 10))); - let texture_atlas = texture_atlas_assets.add(TextureAtlasLayout { + let image = app.world_mut().spawn_asset(make_image(UVec2::new(5, 10))); + let texture_atlas = app.world_mut().spawn_asset(TextureAtlasLayout { size: UVec2::new(5, 10), textures: vec![URect::new(1, 1, 4, 4)], }); @@ -539,6 +558,7 @@ mod tests { }; let anchor = Anchor::BOTTOM_LEFT; + let (image_assets, texture_atlas_assets) = state.get(app.world()).unwrap(); let compute = |point| { sprite.compute_pixel_space_point(point, anchor, &image_assets, &texture_atlas_assets) }; @@ -549,10 +569,9 @@ mod tests { #[test] fn compute_pixel_space_point_for_sprite_with_custom_size_and_rect() { - let mut image_assets = Assets::::default(); - let texture_atlas_assets = Assets::::default(); + let (mut app, mut state) = create_app(); - let image = image_assets.add(make_image(UVec2::new(5, 10))); + let image = app.world_mut().spawn_asset(make_image(UVec2::new(5, 10))); let sprite = Sprite { image, @@ -561,6 +580,7 @@ mod tests { ..Default::default() }; + let (image_assets, texture_atlas_assets) = state.get(app.world()).unwrap(); let compute = |point| { sprite.compute_pixel_space_point( point, @@ -577,10 +597,9 @@ mod tests { #[test] fn compute_pixel_space_point_for_sprite_with_zero_custom_size() { - let mut image_assets = Assets::::default(); - let texture_atlas_assets = Assets::::default(); + let (mut app, mut state) = create_app(); - let image = image_assets.add(make_image(UVec2::new(5, 10))); + let image = app.world_mut().spawn_asset(make_image(UVec2::new(5, 10))); let sprite = Sprite { image, @@ -588,6 +607,7 @@ mod tests { ..Default::default() }; + let (image_assets, texture_atlas_assets) = state.get(app.world()).unwrap(); let compute = |point| { sprite.compute_pixel_space_point( point, diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index 0b5359e58ab5d..5e93a389375b4 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -1,5 +1,5 @@ use crate::{Anchor, Sprite}; -use bevy_asset::Assets; +use bevy_asset::{AssetCommands, Assets, AssetsMut}; use bevy_camera::primitives::Aabb; use bevy_camera::visibility::{ self, NoFrustumCulling, RenderLayers, Visibility, VisibilityClass, VisibleEntities, @@ -162,15 +162,16 @@ impl Default for Text2dShadow { /// /// ## World Resources /// -/// [`ResMut>`](Assets) -- This system only adds new [`Image`] assets. +/// [`AssetsMut`](Assets) -- This system only adds new [`Image`] assets. /// It does not modify or observe existing ones. pub fn update_text2d_layout( mut last_logical_viewport_size: Local, mut target_scale_factors: Local>, // Text2d entities from the previous frame which need to be reprocessed, usually because the font hadn't loaded yet. mut reprocess_queue: Local, - mut textures: ResMut>, - fonts: Res>, + mut asset_commands: AssetCommands, + mut textures: AssetsMut, + fonts: Assets, camera_query: Query<(&Camera, &VisibleEntities, Option<&RenderLayers>)>, mut font_atlas_set: ResMut, mut text_pipeline: ResMut, @@ -216,6 +217,7 @@ pub fn update_text2d_layout( let mut previous_scale_factor = 0.; let mut previous_mask = &RenderLayers::none(); + let mut deferred_font_atlas_set = Default::default(); for ( entity, text2d, @@ -308,7 +310,9 @@ pub fn update_text2d_layout( match text_pipeline.update_text_layout_info( &mut text_layout_info, &mut font_atlas_set, + &mut deferred_font_atlas_set, &mut textures, + &mut asset_commands, &mut computed, &mut scale_cx, text_bounds, @@ -334,6 +338,14 @@ pub fn update_text2d_layout( Ok(()) => {} } } + + for (font_atlas_key, deferred_font_atlas) in deferred_font_atlas_set { + font_atlas_set.entry(font_atlas_key).or_default().extend( + deferred_font_atlas + .into_iter() + .map(|deferred| deferred.to_font_atlas(&mut asset_commands)), + ); + } } /// System calculating and inserting an [`Aabb`] component to entities with some @@ -382,7 +394,9 @@ pub fn calculate_bounds_text2d( mod tests { use bevy_app::{App, Update}; - use bevy_asset::{load_internal_binary_asset, Handle}; + use bevy_asset::{ + load_internal_binary_asset, AssetApp, DirectAssetAccessExt, Handle, MinimalAssetPlugin, + }; use bevy_camera::{ComputedCameraValues, RenderTargetInfo}; use bevy_ecs::schedule::IntoScheduleConfigs; use bevy_math::UVec2; @@ -399,9 +413,10 @@ mod tests { fn setup_with_scale_factor(scale_factor: f32) -> (App, Entity) { let mut app = App::new(); - app.init_resource::>() - .init_resource::>() - .init_resource::>() + app.add_plugins(MinimalAssetPlugin) + .init_asset::() + .init_asset::() + .init_asset::() .init_resource::() .init_resource::() .init_resource::() @@ -446,9 +461,9 @@ mod tests { let world = app.world_mut(); - let mut fonts = world.resource_mut::>(); - - let mut font = fonts.get_mut(bevy_asset::AssetId::default()).unwrap(); + let mut font = world + .get_asset_mut::(bevy_asset::AssetId::default()) + .unwrap(); font.alias = "Fira Mono".into(); let data = font.into_inner().data.clone(); diff --git a/crates/bevy_sprite_render/src/mesh2d/color_material.rs b/crates/bevy_sprite_render/src/mesh2d/color_material.rs index 052be3186012d..2fbd81dffdb57 100644 --- a/crates/bevy_sprite_render/src/mesh2d/color_material.rs +++ b/crates/bevy_sprite_render/src/mesh2d/color_material.rs @@ -1,6 +1,9 @@ use crate::{AlphaMode2d, Material2d, Material2dPlugin}; use bevy_app::{App, Plugin}; -use bevy_asset::{embedded_asset, embedded_path, Asset, AssetApp, AssetPath, Assets, Handle}; +use bevy_asset::{ + embedded_asset, embedded_path, Asset, AssetApp, AssetId, AssetPath, DirectAssetAccessExt, + Handle, +}; use bevy_color::{Alpha, Color, ColorToComponents, LinearRgba}; use bevy_image::Image; use bevy_math::{Affine2, Mat3, Vec4}; @@ -19,16 +22,13 @@ impl Plugin for ColorMaterialPlugin { .register_asset_reflect::(); // Initialize the default material handle. - app.world_mut() - .resource_mut::>() - .insert( - &Handle::::default(), - ColorMaterial { - color: Color::srgb(1.0, 0.0, 1.0), - ..Default::default() - }, - ) - .unwrap(); + app.world_mut().spawn_uuid_asset::( + AssetId::<()>::DEFAULT_UUID, + ColorMaterial { + color: Color::srgb(1.0, 0.0, 1.0), + ..Default::default() + }, + ); } } diff --git a/crates/bevy_sprite_render/src/mesh2d/material.rs b/crates/bevy_sprite_render/src/mesh2d/material.rs index 342e15f992673..a479d4c65da31 100644 --- a/crates/bevy_sprite_render/src/mesh2d/material.rs +++ b/crates/bevy_sprite_render/src/mesh2d/material.rs @@ -81,7 +81,7 @@ pub const MATERIAL_2D_BIND_GROUP_INDEX: usize = 2; /// # use bevy_shader::ShaderRef; /// # use bevy_color::LinearRgba; /// # use bevy_color::palettes::basic::RED; -/// # use bevy_asset::{Handle, AssetServer, Assets, Asset}; +/// # use bevy_asset::{Handle, AssetServer, AssetCommands, Asset}; /// # use bevy_math::primitives::Circle; /// # /// #[derive(AsBindGroup, Debug, Clone, Asset, TypePath)] @@ -108,13 +108,12 @@ pub const MATERIAL_2D_BIND_GROUP_INDEX: usize = 2; /// // Spawn an entity with a mesh using `CustomMaterial`. /// fn setup( /// mut commands: Commands, -/// mut meshes: ResMut>, -/// mut materials: ResMut>, +/// mut asset_commands: AssetCommands, /// asset_server: Res, /// ) { /// commands.spawn(( -/// Mesh2d(meshes.add(Circle::new(50.0))), -/// MeshMaterial2d(materials.add(CustomMaterial { +/// Mesh2d(asset_commands.spawn_asset(Circle::new(50.0).into())), +/// MeshMaterial2d(asset_commands.spawn_asset(CustomMaterial { /// color: RED.into(), /// color_texture: asset_server.load("some_image.png"), /// })), @@ -182,18 +181,17 @@ pub trait Material2d: AsBindGroup + Asset + Clone + Sized { /// # use bevy_ecs::prelude::*; /// # use bevy_mesh::{Mesh, Mesh2d}; /// # use bevy_color::palettes::basic::RED; -/// # use bevy_asset::Assets; +/// # use bevy_asset::AssetCommands; /// # use bevy_math::primitives::Circle; /// # /// // Spawn an entity with a mesh using `ColorMaterial`. /// fn setup( /// mut commands: Commands, -/// mut meshes: ResMut>, -/// mut materials: ResMut>, +/// mut asset_commands: AssetCommands, /// ) { /// commands.spawn(( -/// Mesh2d(meshes.add(Circle::new(50.0))), -/// MeshMaterial2d(materials.add(ColorMaterial::from_color(RED))), +/// Mesh2d(asset_commands.spawn_asset(Circle::new(50.0).into())), +/// MeshMaterial2d(asset_commands.spawn_asset(ColorMaterial::from_color(RED))), /// )); /// } /// ``` diff --git a/crates/bevy_sprite_render/src/mesh2d/wireframe2d.rs b/crates/bevy_sprite_render/src/mesh2d/wireframe2d.rs index ec844c40e4ae5..a854a39dd4699 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, + AssetCommands, AssetEventSystems, AssetId, AssetServer, AssetsMut, Handle, UntypedAssetId, }; use bevy_camera::{visibility::ViewVisibility, Camera, Camera2d}; use bevy_color::{Color, ColorToComponents}; @@ -542,12 +542,12 @@ pub fn extract_wireframe_materials( fn setup_global_wireframe_material( mut commands: Commands, - mut materials: ResMut>, + mut asset_commands: AssetCommands, config: Res, ) { // Create the handle used for the global material commands.insert_resource(GlobalWireframeMaterial { - handle: materials.add(Wireframe2dMaterial { + handle: asset_commands.spawn_asset(Wireframe2dMaterial { color: config.default_color, }), }); @@ -556,7 +556,7 @@ fn setup_global_wireframe_material( /// Updates the wireframe material of all entities without a [`Wireframe2dColor`] or without a [`Wireframe2d`] component fn global_color_changed( config: Res, - mut materials: ResMut>, + mut materials: AssetsMut, global_material: Res, ) { if let Some(mut global_material) = materials.get_mut(&global_material.handle) { @@ -566,14 +566,14 @@ fn global_color_changed( /// Updates the wireframe material when the color in [`Wireframe2dColor`] changes fn wireframe_color_changed( - mut materials: ResMut>, + mut asset_commands: AssetCommands, mut colors_changed: Query< (&mut Mesh2dWireframe, &Wireframe2dColor), (With, Changed), >, ) { for (mut handle, wireframe_color) in &mut colors_changed { - handle.0 = materials.add(Wireframe2dMaterial { + handle.0 = asset_commands.spawn_asset(Wireframe2dMaterial { color: wireframe_color.color, }); } @@ -583,7 +583,7 @@ fn wireframe_color_changed( /// for any mesh with a [`NoWireframe2d`] component. fn apply_wireframe_material( mut commands: Commands, - mut materials: ResMut>, + mut asset_commands: AssetCommands, wireframes: Query< (Entity, Option<&Wireframe2dColor>), (With, Without), @@ -600,7 +600,7 @@ fn apply_wireframe_material( let mut material_to_spawn = vec![]; for (e, maybe_color) in &wireframes { - let material = get_wireframe_material(maybe_color, &mut materials, &global_material); + let material = get_wireframe_material(maybe_color, &global_material, &mut asset_commands); material_to_spawn.push((e, Mesh2dWireframe(material))); } commands.try_insert_batch(material_to_spawn); @@ -611,6 +611,7 @@ type WireframeFilter = (With, Without, Without, meshes_without_material: Query< (Entity, Option<&Wireframe2dColor>), @@ -618,12 +619,12 @@ fn apply_global_wireframe_material( >, meshes_with_global_material: Query)>, global_material: Res, - mut materials: ResMut>, ) { if config.global { let mut material_to_spawn = vec![]; for (e, maybe_color) in &meshes_without_material { - let material = get_wireframe_material(maybe_color, &mut materials, &global_material); + let material = + get_wireframe_material(maybe_color, &global_material, &mut asset_commands); // We only add the material handle but not the Wireframe component // This makes it easy to detect which mesh is using the global material and which ones are user specified material_to_spawn.push((e, Mesh2dWireframe(material))); @@ -639,11 +640,11 @@ fn apply_global_wireframe_material( /// Gets a handle to a wireframe material with a fallback on the default material fn get_wireframe_material( maybe_color: Option<&Wireframe2dColor>, - wireframe_materials: &mut Assets, global_material: &GlobalWireframeMaterial, + asset_commands: &mut AssetCommands, ) -> Handle { if let Some(wireframe_color) = maybe_color { - wireframe_materials.add(Wireframe2dMaterial { + asset_commands.spawn_asset(Wireframe2dMaterial { color: wireframe_color.color, }) } else { diff --git a/crates/bevy_sprite_render/src/render/mod.rs b/crates/bevy_sprite_render/src/render/mod.rs index 55cadf0870eec..28d878b1be5b6 100644 --- a/crates/bevy_sprite_render/src/render/mod.rs +++ b/crates/bevy_sprite_render/src/render/mod.rs @@ -360,7 +360,7 @@ pub fn extract_sprite_events( pub fn extract_sprites( mut extracted_sprites: ResMut, mut extracted_slices: ResMut, - texture_atlases: Extract>>, + texture_atlases: Extract>, sprite_query: Extract< Query<( Entity, diff --git a/crates/bevy_sprite_render/src/sprite_mesh/mod.rs b/crates/bevy_sprite_render/src/sprite_mesh/mod.rs index 671e568a83761..5e326cb30e28e 100644 --- a/crates/bevy_sprite_render/src/sprite_mesh/mod.rs +++ b/crates/bevy_sprite_render/src/sprite_mesh/mod.rs @@ -3,10 +3,10 @@ use bevy_ecs::{ entity::Entity, query::{Added, Changed, Or}, schedule::IntoScheduleConfigs, - system::{Commands, Local, Query, Res, ResMut}, + system::{Commands, Local, Query}, }; -use bevy_asset::{Assets, Handle}; +use bevy_asset::{AssetCommands, Assets, Handle}; use bevy_image::TextureAtlasLayout; use bevy_math::{primitives::Rectangle, vec2}; @@ -39,12 +39,12 @@ impl Plugin for SpriteMeshPlugin { // The meshhandle is kept locally so they can be cloned. fn add_mesh( sprites: Query>, - mut meshes: ResMut>, mut quad: Local>>, mut commands: Commands, + mut asset_commands: AssetCommands, ) { let quad = quad.get_or_insert_with(|| { - meshes.add( + asset_commands.spawn_asset( Rectangle::from_size(vec2(1.0, 1.0)) .mesh() .build() @@ -71,10 +71,10 @@ fn add_material( (Entity, &SpriteMesh, &Anchor), Or<(Changed, Changed, Added)>, >, - texture_atlas_layouts: Res>, + texture_atlas_layouts: Assets, mut cached_materials: Local>>, - mut materials: ResMut>, mut commands: Commands, + mut asset_commands: AssetCommands, ) { for (entity, sprite, anchor) in sprites { if let Some(handle) = cached_materials.get(&(sprite.clone(), *anchor)) { @@ -93,7 +93,7 @@ fn add_material( material.texture_atlas_index = texture_atlas.index; } - let handle = materials.add(material); + let handle = asset_commands.spawn_asset(material); cached_materials.insert((sprite.clone(), *anchor), handle.clone()); commands diff --git a/crates/bevy_sprite_render/src/texture_slice/computed_slices.rs b/crates/bevy_sprite_render/src/texture_slice/computed_slices.rs index 55324faa658b0..55ff1426ea518 100644 --- a/crates/bevy_sprite_render/src/texture_slice/computed_slices.rs +++ b/crates/bevy_sprite_render/src/texture_slice/computed_slices.rs @@ -109,8 +109,8 @@ fn compute_sprite_slices( pub(crate) fn compute_slices_on_asset_event( mut commands: Commands, mut events: MessageReader>, - images: Res>, - atlas_layouts: Res>, + images: Assets, + atlas_layouts: Assets, sprites: Query<(Entity, &Sprite)>, ) { // We store the asset ids of added/modified image assets @@ -141,8 +141,8 @@ pub(crate) fn compute_slices_on_asset_event( /// System reacting to changes on the [`Sprite`] component to compute the sprite slices pub(crate) fn compute_slices_on_sprite_change( mut commands: Commands, - images: Res>, - atlas_layouts: Res>, + images: Assets, + atlas_layouts: Assets, changed_sprites: Query<(Entity, &Sprite), Changed>, ) { for (entity, sprite) in &changed_sprites { diff --git a/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs b/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs index d8353466ff7b4..b559a81cc52c6 100644 --- a/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs +++ b/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs @@ -1,6 +1,6 @@ use crate::{AlphaMode2d, MeshMaterial2d}; use bevy_app::{App, Plugin, Update}; -use bevy_asset::{Assets, Handle}; +use bevy_asset::{AssetsMut, Handle, WorldAssetCommandsExt}; use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ @@ -10,7 +10,7 @@ use bevy_ecs::{ query::Changed, reflect::{ReflectComponent, ReflectResource}, resource::Resource, - system::{Query, ResMut}, + system::Query, template::FromTemplate, world::DeferredWorld, }; @@ -172,19 +172,21 @@ fn on_insert_tilemap_chunk(mut world: DeferredWorld, HookContext { entity, .. }: let mesh = if let Some(mesh) = tilemap_chunk_mesh_cache.get(&mesh_size) { mesh.clone() } else { - let mut meshes = world.resource_mut::>(); - meshes.add(Rectangle::from_size(mesh_size.as_vec2())) + world + .asset_commands() + .spawn_asset(Mesh::from(Rectangle::from_size(mesh_size.as_vec2()))) }; - let mut images = world.resource_mut::>(); - let tile_data = images.add(tile_data_image); + let mut asset_commands = world.asset_commands(); - let mut materials = world.resource_mut::>(); - let material = materials.add(TilemapChunkMaterial { + let tile_data = asset_commands.spawn_asset(tile_data_image); + + let material = asset_commands.spawn_asset(TilemapChunkMaterial { tileset, tile_data, alpha_mode, }); + drop(asset_commands); world .commands() @@ -202,8 +204,8 @@ pub fn update_tilemap_chunk_indices( ), Changed, >, - mut materials: ResMut>, - mut images: ResMut>, + mut materials: AssetsMut, + mut images: AssetsMut, ) { for (chunk_entity, TilemapChunk { chunk_size, .. }, tile_data, material) in query { let expected_tile_data_length = chunk_size.element_product() as usize; diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index 58103af8a2a94..6b9139785a919 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -2,11 +2,13 @@ use crate::FontCx; use crate::FontSource; use crate::TextFont; use bevy_asset::Asset; -use bevy_asset::AssetId; -use bevy_asset::Assets; +use bevy_asset::AssetEntity; +use bevy_asset::AssetUuidMap; +use bevy_asset::AssetsMut; use bevy_ecs::change_detection::DetectChangesMut; use bevy_ecs::system::Local; use bevy_ecs::system::Query; +use bevy_ecs::system::Res; use bevy_ecs::system::ResMut; use bevy_platform::collections::HashSet; use bevy_reflect::TypePath; @@ -49,31 +51,37 @@ impl Font { /// Font asset changes are track locally instead of waiting for asset events. Text layout also builds the atlas images, and waiting for asset events would /// delay the image updates by a frame. pub fn load_font_assets_into_font_collection( - mut fonts: ResMut>, - mut loaded_fonts: Local>>, + mut fonts: AssetsMut, + mut loaded_fonts: Local>, mut font_cx: ResMut, mut text_font_query: Query<&mut TextFont>, + uuid_map: Res, ) { let font_removed = loaded_fonts.iter().any(|id| !fonts.contains(*id)); - let new_asset_ids: Vec<_> = if font_removed { + let new_asset_entities: Vec<_> = if font_removed { // If any font asset has been removed, clear the font collection and queue the remaining fonts to be reinserted into the collection. font_cx.collection.clear(); loaded_fonts.clear(); - loaded_fonts.extend(fonts.ids()); + loaded_fonts.extend(fonts.iter().map(|(entity, _)| entity)); loaded_fonts.iter().copied().collect() } else { - fonts.ids().filter(|id| loaded_fonts.insert(*id)).collect() + fonts + .iter() + .map(|(id, _)| id) + .filter(|id| loaded_fonts.insert(*id)) + .collect() }; - if new_asset_ids.is_empty() && !font_removed { + if new_asset_entities.is_empty() && !font_removed { return; } let mut new_family_ids = Vec::new(); - for asset_id in &new_asset_ids { - let font = fonts - .get_mut_untracked(*asset_id) + for asset_id in &new_asset_entities { + let mut font = fonts + .get_mut(*asset_id) .expect("Each AssetId should have a corresponding asset"); + let font = font.bypass_change_detection(); font.alias = format!("asset_id:{asset_id:?}"); @@ -105,7 +113,11 @@ pub fn load_font_assets_into_font_collection( for mut text_font in text_font_query.iter_mut() { if font_removed || match &text_font.font { - FontSource::Handle(handle) => new_asset_ids.contains(&handle.id()), + FontSource::Handle(handle) => uuid_map + .resolve_entity(handle) + .ok() + .map(|entity| new_asset_entities.contains(&entity)) + .unwrap_or(false), FontSource::Family(name) => font_cx .collection .family_id(name) @@ -142,34 +154,33 @@ pub fn load_font_assets_into_font_collection( #[cfg(test)] mod tests { use bevy_app::{App, Update}; - use bevy_asset::Assets; + use bevy_asset::{DirectAssetAccessExt, MinimalAssetPlugin}; use super::*; #[test] fn font_asset_registration_and_cleanup() { let mut app = App::new(); - app.init_resource::>() + app.add_plugins(MinimalAssetPlugin) .init_resource::() .add_systems(Update, load_font_assets_into_font_collection); - let font_handle = app - .world_mut() - .resource_mut::>() - .add(Font::from_bytes( - include_bytes!("FiraMono-subset.ttf").to_vec(), - )); + let font_handle = app.world_mut().spawn_asset(Font::from_bytes( + include_bytes!("FiraMono-subset.ttf").to_vec(), + )); app.update(); let world = app.world_mut(); let font_alias = world - .resource::>() - .get(&font_handle) + .get_asset(font_handle.id()) .expect("The font asset was just added above.") .alias .clone(); - assert_eq!(font_alias, format!("asset_id:{:?}", font_handle.id())); + assert_eq!( + font_alias, + format!("asset_id:{:?}", font_handle.entity().unwrap()) + ); assert!(world .resource_mut::() .collection @@ -181,9 +192,7 @@ mod tests { .family_id(&font_alias) .is_some()); - world - .resource_mut::>() - .remove(font_handle.id()); + world.remove_asset(font_handle.id()).unwrap(); app.update(); let world = app.world_mut(); diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index 92bef4ef048bd..f9044fdabcdbb 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -1,4 +1,4 @@ -use bevy_asset::{Assets, Handle, RenderAssetUsages}; +use bevy_asset::{AssetCommands, AssetsMut, Handle, RenderAssetUsages}; use bevy_image::{prelude::*, ImageSampler, ToExtents}; use bevy_math::{UVec2, Vec2}; use bevy_platform::collections::HashMap; @@ -38,13 +38,95 @@ pub struct FontAtlas { } impl FontAtlas { - /// Create a new [`FontAtlas`] with the given size, adding it to the appropriate asset collections. + /// Get the [`GlyphAtlasLocation`] for a subpixel-offset glyph. + pub fn get_glyph_index(&self, cache_key: GlyphCacheKey) -> Option { + self.glyph_to_atlas_index.get(&cache_key).copied() + } + + /// Checks if the given subpixel-offset glyph is contained in this [`FontAtlas`]. + pub fn has_glyph(&self, cache_key: GlyphCacheKey) -> bool { + self.glyph_to_atlas_index.contains_key(&cache_key) + } + + /// Add a glyph to the atlas, updating both its texture and layout. + /// + /// The glyph is represented by `key`, and its image content is `texture`. + /// This content is copied into the atlas texture, and the atlas layout is updated + /// to store the location of that glyph into the atlas. + /// + /// # Returns + /// + /// Returns `()` if the glyph is successfully added, or [`TextError::FailedToAddGlyph`] otherwise. + /// In that case, neither the atlas texture nor the atlas layout are + /// modified. + pub fn add_glyph( + &mut self, + textures: &mut AssetsMut, + key: GlyphCacheKey, + texture: &Image, + offset: Vec2, + is_alpha_mask: bool, + ) -> Result<(), TextError> { + let mut atlas_texture = textures + .get_mut(&self.texture) + .ok_or(TextError::MissingAtlasTexture)?; + + if let Ok(glyph_index) = self.dynamic_texture_atlas_builder.add_texture( + &mut self.texture_atlas, + texture, + &mut atlas_texture, + ) { + self.glyph_to_atlas_index.insert( + key, + GlyphAtlasLocation { + glyph_index, + offset, + is_alpha_mask, + }, + ); + Ok(()) + } else { + Err(TextError::FailedToAddGlyph(key.glyph_id)) + } + } +} + +impl core::fmt::Debug for FontAtlas { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("FontAtlas") + .field("glyph_to_atlas_index", &self.glyph_to_atlas_index) + .field("texture_atlas", &self.texture_atlas) + .field("texture", &self.texture) + .field("dynamic_texture_atlas_builder", &"[...]") + .finish() + } +} + +/// A version of [`FontAtlas`] which hasn't been "submitted" yet. +/// +/// This holds owned instances of the texture and texture atlas, which need to be sent to the ECS. +pub struct DeferredFontAtlas { + /// Used to update the [`TextureAtlasLayout`]. + pub dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder, + /// A mapping between subpixel-offset glyphs and their [`GlyphAtlasLocation`]. + pub glyph_to_atlas_index: HashMap, + /// The [`TextureAtlasLayout`] that holds the rasterized glyphs. + pub texture_atlas: TextureAtlasLayout, + /// The texture for this font atlas. + pub texture: Image, + /// The handle into which the texture will be inserted. + pub texture_handle: Handle, +} + +impl DeferredFontAtlas { + /// Create a new [`DeferredFontAtlas`] with the given size, adding it to the appropriate asset + /// collections. pub fn new( - textures: &mut Assets, + asset_commands: &mut AssetCommands, size: UVec2, font_smoothing: FontSmoothing, - ) -> FontAtlas { - let mut image = Image::new_fill( + ) -> Self { + let mut texture = Image::new_fill( size.to_extents(), TextureDimension::D2, &[0, 0, 0, 0], @@ -53,14 +135,14 @@ impl FontAtlas { RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD, ); if font_smoothing == FontSmoothing::None { - image.sampler = ImageSampler::nearest(); + texture.sampler = ImageSampler::nearest(); } - let texture = textures.add(image); Self { - texture_atlas: TextureAtlasLayout::new_empty(size), glyph_to_atlas_index: HashMap::default(), dynamic_texture_atlas_builder: DynamicTextureAtlasBuilder::new(size, 2), + texture_atlas: TextureAtlasLayout::new_empty(size), texture, + texture_handle: asset_commands.reserve_handle(), } } @@ -76,7 +158,7 @@ impl FontAtlas { /// Add a glyph to the atlas, updating both its texture and layout. /// - /// The glyph is represented by `glyph`, and its image content is `glyph_texture`. + /// The glyph is represented by `key`, and its image content is `texture`. /// This content is copied into the atlas texture, and the atlas layout is updated /// to store the location of that glyph into the atlas. /// @@ -87,20 +169,15 @@ impl FontAtlas { /// modified. pub fn add_glyph( &mut self, - textures: &mut Assets, key: GlyphCacheKey, texture: &Image, offset: Vec2, is_alpha_mask: bool, ) -> Result<(), TextError> { - let mut atlas_texture = textures - .get_mut(&self.texture) - .ok_or(TextError::MissingAtlasTexture)?; - if let Ok(glyph_index) = self.dynamic_texture_atlas_builder.add_texture( &mut self.texture_atlas, texture, - &mut atlas_texture, + &mut self.texture, ) { self.glyph_to_atlas_index.insert( key, @@ -115,9 +192,22 @@ impl FontAtlas { Err(TextError::FailedToAddGlyph(key.glyph_id)) } } + + /// Converts this "deferred" instance of a font atlas into an actual [`FontAtlas`]. + /// + /// Its internal assets (texture + texture atlas) are spawned as assets. + pub fn to_font_atlas(self, asset_commands: &mut AssetCommands) -> FontAtlas { + asset_commands.insert_asset(&self.texture_handle, self.texture); + FontAtlas { + dynamic_texture_atlas_builder: self.dynamic_texture_atlas_builder, + glyph_to_atlas_index: self.glyph_to_atlas_index, + texture_atlas: self.texture_atlas, + texture: self.texture_handle, + } + } } -impl core::fmt::Debug for FontAtlas { +impl core::fmt::Debug for DeferredFontAtlas { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("FontAtlas") .field("glyph_to_atlas_index", &self.glyph_to_atlas_index) @@ -130,8 +220,10 @@ impl core::fmt::Debug for FontAtlas { /// Adds the given subpixel-offset glyph to the given font atlases pub fn add_glyph_to_atlas( - font_atlases: &mut Vec, - textures: &mut Assets, + font_atlases: &mut [FontAtlas], + deferred_font_atlases: &mut Vec, + textures: &mut AssetsMut, + asset_commands: &mut AssetCommands, scaler: &mut Scaler, font_smoothing: FontSmoothing, glyph_id: u16, @@ -147,9 +239,20 @@ pub fn add_glyph_to_atlas( is_alpha_mask, ) }; + let add_char_to_deferred_font_atlas = |atlas: &mut DeferredFontAtlas| -> Result<(), TextError> { + atlas.add_glyph( + GlyphCacheKey { glyph_id }, + &glyph_texture, + offset, + is_alpha_mask, + ) + }; if !font_atlases .iter_mut() .any(|atlas| add_char_to_font_atlas(atlas).is_ok()) + && !deferred_font_atlases + .iter_mut() + .any(|atlas| add_char_to_deferred_font_atlas(atlas).is_ok()) { // Find the largest dimension of the glyph, either its width or its height let glyph_max_size: u32 = glyph_texture @@ -160,21 +263,24 @@ pub fn add_glyph_to_atlas( // Pick the higher of 512 or the smallest power of 2 greater than glyph_max_size let containing = (1u32 << (32 - glyph_max_size.leading_zeros())).max(512); - let mut new_atlas = FontAtlas::new(textures, UVec2::splat(containing), font_smoothing); - + let mut new_atlas = + DeferredFontAtlas::new(asset_commands, UVec2::splat(containing), font_smoothing); new_atlas.add_glyph( - textures, GlyphCacheKey { glyph_id }, &glyph_texture, offset, is_alpha_mask, )?; - font_atlases.push(new_atlas); + deferred_font_atlases.push(new_atlas); } - get_glyph_atlas_info(font_atlases, GlyphCacheKey { glyph_id }) - .ok_or(TextError::InconsistentAtlasState) + get_glyph_atlas_info( + font_atlases, + deferred_font_atlases, + GlyphCacheKey { glyph_id }, + ) + .ok_or(TextError::InconsistentAtlasState) } /// Get the texture of the glyph as a rendered image, and its offset @@ -251,17 +357,32 @@ pub fn get_outlined_glyph_texture( /// Generates the [`GlyphAtlasInfo`] for the given subpixel-offset glyph. pub fn get_glyph_atlas_info( - font_atlases: &mut [FontAtlas], + font_atlases: &[FontAtlas], + deferred_atlases: &[DeferredFontAtlas], cache_key: GlyphCacheKey, ) -> Option { - font_atlases.iter().find_map(|atlas| { - atlas - .get_glyph_index(cache_key) - .map(|location| GlyphAtlasInfo { - offset: location.offset, - rect: atlas.texture_atlas.textures[location.glyph_index].as_rect(), - texture: atlas.texture.id(), - is_alpha_mask: location.is_alpha_mask, + font_atlases + .iter() + .find_map(|atlas| { + atlas + .get_glyph_index(cache_key) + .map(|location| GlyphAtlasInfo { + offset: location.offset, + rect: atlas.texture_atlas.textures[location.glyph_index].as_rect(), + texture: atlas.texture.id(), + is_alpha_mask: location.is_alpha_mask, + }) + }) + .or_else(|| { + deferred_atlases.iter().find_map(|atlas| { + atlas + .get_glyph_index(cache_key) + .map(|location| GlyphAtlasInfo { + offset: location.offset, + rect: atlas.texture_atlas.textures[location.glyph_index].as_rect(), + texture: atlas.texture_handle.id(), + is_alpha_mask: location.is_alpha_mask, + }) }) - }) + }) } diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index e6ebb42f97a76..584de0f649b76 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -140,10 +140,10 @@ impl Plugin for TextPlugin { #[cfg(feature = "default_font")] { - use bevy_asset::{AssetId, Assets}; - let mut assets = app.world_mut().resource_mut::>(); + use bevy_asset::{AssetId, DirectAssetAccessExt}; let asset = Font::from_bytes(DEFAULT_FONT_DATA.to_vec()); - assets.insert(AssetId::default(), asset).unwrap(); + app.world_mut() + .spawn_uuid_asset(AssetId::<()>::DEFAULT_UUID, asset); }; } } diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index ac19fbebdd1b1..b9620c915b92f 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -1,8 +1,9 @@ use alloc::borrow::Cow; +use bevy_platform::collections::HashMap; use core::hash::BuildHasher; -use bevy_asset::Assets; +use bevy_asset::{AssetCommands, Assets, AssetsMut}; use bevy_color::Color; use bevy_ecs::{ component::Component, entity::Entity, reflect::ReflectComponent, resource::Resource, @@ -25,9 +26,9 @@ use crate::{ error::TextError, get_glyph_atlas_info, parley_context::{FontCx, LayoutCx, ScaleCx}, - ComputedTextBlock, Font, FontAtlasKey, FontAtlasSet, FontHinting, FontSmoothing, FontSource, - Justify, LetterSpacing, LineBreak, LineHeight, PositionedGlyph, TextBounds, TextEntity, - TextFont, TextLayout, + ComputedTextBlock, DeferredFontAtlas, Font, FontAtlasKey, FontAtlasSet, FontHinting, + FontSmoothing, FontSource, Justify, LetterSpacing, LineBreak, LineHeight, PositionedGlyph, + TextBounds, TextEntity, TextFont, TextLayout, }; struct TextSectionView<'a> { @@ -305,7 +306,9 @@ impl TextPipeline { &mut self, layout_info: &mut TextLayoutInfo, font_atlas_set: &mut FontAtlasSet, - textures: &mut Assets, + deferred_font_atlas_set: &mut HashMap>, + textures: &mut AssetsMut, + asset_commands: &mut AssetCommands, computed: &mut ComputedTextBlock, scale_cx: &mut ScaleCx, bounds: TextBounds, @@ -359,18 +362,25 @@ impl TextPipeline { }; let font_atlases = font_atlas_set.entry(font_atlas_key).or_default(); - let atlas_info = - get_glyph_atlas_info(font_atlases, crate::GlyphCacheKey { glyph_id }) - .map(Ok) - .unwrap_or_else(|| { - add_glyph_to_atlas( - font_atlases, - textures, - &mut scaler, - font_smoothing, - glyph_id, - ) - })?; + let deferred_font_atlases = + deferred_font_atlas_set.entry(font_atlas_key).or_default(); + let atlas_info = get_glyph_atlas_info( + font_atlases, + deferred_font_atlases, + crate::GlyphCacheKey { glyph_id }, + ) + .map(Ok) + .unwrap_or_else(|| { + add_glyph_to_atlas( + font_atlases, + deferred_font_atlases, + textures, + asset_commands, + &mut scaler, + font_smoothing, + glyph_id, + ) + })?; let glyph_pos = Vec2::new(glyph.x, glyph.y); let size = atlas_info.rect.size(); diff --git a/crates/bevy_ui/src/widget/image.rs b/crates/bevy_ui/src/widget/image.rs index 8b5a389510191..25fc8fa652ec4 100644 --- a/crates/bevy_ui/src/widget/image.rs +++ b/crates/bevy_ui/src/widget/image.rs @@ -133,7 +133,7 @@ impl ImageNode { /// # Example /// /// ``` - /// use bevy_asset::{Assets,AssetServer}; + /// use bevy_asset::{Assets,AssetServer,AssetCommands}; /// use bevy_ecs::prelude::{Commands,Res,ResMut}; /// use bevy_image::{TextureAtlas,TextureAtlasLayout}; /// use bevy_math::{UVec2,Rect}; @@ -143,12 +143,12 @@ impl ImageNode { /// /// fn setup( /// mut commands: Commands, + /// mut asset_commands: AssetCommands, /// asset_server: Res, - /// mut texture_atlas_layouts: ResMut>, /// ) { /// let texture = asset_server.load("textures/array_texture.png"); /// let layout = TextureAtlasLayout::from_grid(UVec2::splat(250), 1, 3, None, None); - /// let texture_atlas_layout = texture_atlas_layouts.add(layout); + /// let texture_atlas_layout = asset_commands.spawn_asset(layout); /// /// commands.spawn(Node { /// display: Display::Flex, @@ -367,8 +367,8 @@ type UpdateImageFilter = (With, Without); /// Updates content size of the node based on the image provided pub fn update_image_content_size_system( - textures: Res>, - atlases: Res>, + textures: Assets, + atlases: Assets, mut query: Query< ( &mut ContentSize, diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 9a6e7e9c6b7a5..a666d64caf655 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -2,7 +2,7 @@ use crate::{ ComputedNode, ComputedUiRenderTargetInfo, ContentSize, FixedMeasure, Measure, MeasureArgs, Node, NodeMeasure, }; -use bevy_asset::Assets; +use bevy_asset::{AssetCommands, Assets, AssetsMut}; use bevy_color::Color; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ @@ -242,7 +242,7 @@ impl Measure for TextMeasure { /// color changes. This can be expensive, particularly for large blocks of text, and the [`bypass_change_detection`](bevy_ecs::change_detection::DetectChangesMut::bypass_change_detection) /// method should be called when only changing the `Text`'s colors. pub fn measure_text_system( - fonts: Res>, + fonts: Assets, mut text_query: Query< ( Entity, @@ -287,7 +287,7 @@ pub fn measure_text_system( match text_pipeline.create_text_measure( entity, - fonts.as_ref(), + &fonts, text_reader.iter(entity), computed_target.scale_factor, &block, @@ -335,10 +335,11 @@ pub fn measure_text_system( /// /// ## World Resources /// -/// [`ResMut>`](Assets) -- This system only adds new [`Image`] assets. +/// [`AssetsMut`](Assets) -- This system only adds new [`Image`] assets. /// It does not modify or observe existing ones. The exception is when adding new glyphs to a [`bevy_text::FontAtlas`]. pub fn text_system( - mut textures: ResMut>, + mut textures: AssetsMut, + mut asset_commands: AssetCommands, mut font_atlas_set: ResMut, mut text_pipeline: ResMut, mut text_query: Query<( @@ -351,6 +352,7 @@ pub fn text_system( )>, mut scale_cx: ResMut, ) { + let mut deferred_font_atlas_set = Default::default(); for (node, block, mut text_layout_info, mut text_flags, mut computed, hinting) in &mut text_query { @@ -371,7 +373,9 @@ pub fn text_system( match text_pipeline.update_text_layout_info( &mut text_layout_info, &mut font_atlas_set, + &mut deferred_font_atlas_set, &mut textures, + &mut asset_commands, &mut computed, &mut scale_cx, physical_node_size, @@ -405,4 +409,12 @@ pub fn text_system( } } } + + for (font_atlas_key, deferred_font_atlas) in deferred_font_atlas_set { + font_atlas_set.entry(font_atlas_key).or_default().extend( + deferred_font_atlas + .into_iter() + .map(|deferred| deferred.to_font_atlas(&mut asset_commands)), + ); + } } diff --git a/crates/bevy_ui/src/widget/text_input_layout.rs b/crates/bevy_ui/src/widget/text_input_layout.rs index 6f5ca8e241457..8b04319a7b85d 100644 --- a/crates/bevy_ui/src/widget/text_input_layout.rs +++ b/crates/bevy_ui/src/widget/text_input_layout.rs @@ -2,7 +2,7 @@ use core::hash::BuildHasher; use core::time::Duration; use crate::{ComputedNode, ComputedUiRenderTargetInfo, ContentSize, NodeMeasure}; -use bevy_asset::Assets; +use bevy_asset::{AssetCommands, Assets, AssetsMut}; use bevy_ecs::{ change_detection::{DetectChanges, DetectChangesMut}, @@ -15,7 +15,7 @@ use bevy_ecs::{ use bevy_image::prelude::*; use bevy_input_focus::InputFocus; use bevy_math::{Rect, Vec2}; -use bevy_platform::hash::FixedHasher; +use bevy_platform::{collections::HashMap, hash::FixedHasher}; use bevy_reflect::std_traits::ReflectDefault; use bevy_reflect::Reflect; use bevy_text::{ @@ -73,7 +73,7 @@ pub fn update_editable_text_content_size( Ref, &mut ContentSize, )>, - fonts: Res>, + fonts: Assets, mut font_cx: ResMut, rem_size: Res, ) { @@ -90,7 +90,7 @@ pub fn update_editable_text_content_size( let font_size = text_font.font_size.eval(target.logical_size(), rem_size.0); let width = editable_text.visible_width.and_then(|visible_width| { - let resolved_font = resolve_font_source(&text_font, fonts.as_ref()).ok()?; + let resolved_font = resolve_font_source(&text_font, &fonts).ok()?; let font_context = &mut font_cx.context; let mut query = font_context .collection @@ -161,7 +161,7 @@ pub fn update_editable_text_content_size( /// Syncs each [`EditableText`] entity's [`PlainEditor`](parley::PlainEditor) /// style properties to match its [`TextFont`], [`LineHeight`], and [`TextLayout`] components. pub fn update_editable_text_styles( - fonts: Res>, + fonts: Assets, mut editable_text_query: Query<( &mut EditableText, Ref, @@ -194,7 +194,7 @@ pub fn update_editable_text_styles( } if text_font.is_changed() { - let Ok(resolved_font) = resolve_font_source(&text_font, fonts.as_ref()) else { + let Ok(resolved_font) = resolve_font_source(&text_font, &fonts) else { continue; }; @@ -261,7 +261,8 @@ pub fn update_editable_text_layout( mut layout_cx: ResMut, mut scale_cx: ResMut, mut font_atlas_set: ResMut, - mut textures: ResMut>, + mut textures: AssetsMut, + mut asset_commands: AssetCommands, mut input_field_query: Query<( Entity, &TextFont, @@ -322,6 +323,7 @@ pub fn update_editable_text_layout( info.glyphs.clear(); info.run_geometry.clear(); + let mut deferred_atlases = HashMap::<_, Vec<_>>::new(); for (line_index, line) in layout.lines().enumerate() { for item in line.items() { match item { @@ -346,8 +348,11 @@ pub fn update_editable_text_layout( for glyph in glyph_run.positioned_glyphs() { let font_atlases = font_atlas_set.entry(font_atlas_key).or_default(); + let deferred_atlases = + deferred_atlases.entry(font_atlas_key).or_default(); let Ok(atlas_info) = get_glyph_atlas_info( font_atlases, + deferred_atlases, GlyphCacheKey { glyph_id: glyph.id as u16, }, @@ -367,7 +372,9 @@ pub fn update_editable_text_layout( .build(); add_glyph_to_atlas( font_atlases, - textures.as_mut(), + deferred_atlases, + &mut textures, + &mut asset_commands, &mut scaler, text_font.font_smoothing, glyph.id as u16, diff --git a/crates/bevy_ui/src/widget/viewport.rs b/crates/bevy_ui/src/widget/viewport.rs index 5668f94ddda60..1c779cc9cfae3 100644 --- a/crates/bevy_ui/src/widget/viewport.rs +++ b/crates/bevy_ui/src/widget/viewport.rs @@ -1,7 +1,7 @@ #[cfg(feature = "bevy_picking")] use crate::UiGlobalTransform; use crate::{ComputedNode, Node}; -use bevy_asset::Assets; +use bevy_asset::AssetsMut; #[cfg(feature = "bevy_picking")] use bevy_camera::Camera; use bevy_camera::RenderTarget; @@ -10,7 +10,7 @@ use bevy_ecs::{ entity::Entity, query::{Changed, Or}, reflect::ReflectComponent, - system::{Query, ResMut}, + system::Query, }; #[cfg(feature = "bevy_picking")] use bevy_ecs::{ @@ -169,7 +169,7 @@ pub fn update_viewport_render_target_size( Or<(Changed, Changed)>, >, camera_query: Query<&RenderTarget>, - mut images: ResMut>, + mut images: AssetsMut, ) { for (mut viewport, computed_node) in &mut viewport_query { let Some(camera) = viewport.camera else { diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index a1efacc959275..5398281809641 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -488,7 +488,7 @@ pub fn extract_uinode_background_colors( pub fn extract_uinode_images( mut commands: Commands, mut extracted_uinodes: ResMut, - texture_atlases: Extract>>, + texture_atlases: Extract>, uinode_query: Extract< Query<( Entity, diff --git a/crates/bevy_ui_render/src/ui_material.rs b/crates/bevy_ui_render/src/ui_material.rs index e716a600c8c02..5746dc9a011df 100644 --- a/crates/bevy_ui_render/src/ui_material.rs +++ b/crates/bevy_ui_render/src/ui_material.rs @@ -35,7 +35,7 @@ use derive_more::derive::From; /// # use bevy_render::render_resource::AsBindGroup; /// # use bevy_color::LinearRgba; /// # use bevy_shader::ShaderRef; -/// # use bevy_asset::{Handle, AssetServer, Assets, Asset}; +/// # use bevy_asset::{Handle, AssetServer, AssetCommands, Asset}; /// # use bevy_ui_render::prelude::*; /// /// #[derive(AsBindGroup, Asset, TypePath, Debug, Clone)] @@ -60,9 +60,9 @@ use derive_more::derive::From; /// } /// /// // Spawn an entity using `CustomMaterial`. -/// fn setup(mut commands: Commands, mut materials: ResMut>, asset_server: Res) { +/// fn setup(mut commands: Commands, mut asset_commands: AssetCommands, asset_server: Res) { /// commands.spawn(( -/// MaterialNode(materials.add(CustomMaterial { +/// MaterialNode(asset_commands.spawn_asset(CustomMaterial { /// color: LinearRgba::RED, /// color_texture: asset_server.load("some_image.png"), /// })), diff --git a/crates/bevy_ui_render/src/ui_material_pipeline.rs b/crates/bevy_ui_render/src/ui_material_pipeline.rs index d9cbbb520c333..f80f86a65b787 100644 --- a/crates/bevy_ui_render/src/ui_material_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_material_pipeline.rs @@ -321,7 +321,7 @@ impl Default for ExtractedUiMaterialNodes { pub fn extract_ui_material_nodes( mut commands: Commands, mut extracted_uinodes: ResMut>, - materials: Extract>>, + materials: Extract>, uinode_query: Extract< Query<( Entity, diff --git a/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs b/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs index fa6f4f3aff48b..39869e511e4c3 100644 --- a/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs @@ -215,7 +215,7 @@ pub struct ExtractedUiTextureSlices { pub fn extract_ui_texture_slices( mut commands: Commands, mut extracted_ui_slicers: ResMut, - texture_atlases: Extract>>, + texture_atlases: Extract>, slicers_query: Extract< Query<( Entity, diff --git a/crates/bevy_winit/src/cursor/custom_cursor.rs b/crates/bevy_winit/src/cursor/custom_cursor.rs index d754e8a5a3f4d..c75c0beabcab5 100644 --- a/crates/bevy_winit/src/cursor/custom_cursor.rs +++ b/crates/bevy_winit/src/cursor/custom_cursor.rs @@ -166,7 +166,8 @@ pub(crate) fn transform_hotspot( #[cfg(test)] mod tests { use bevy_app::App; - use bevy_asset::RenderAssetUsages; + use bevy_asset::{AssetApp, DirectAssetAccessExt, MinimalAssetPlugin, RenderAssetUsages}; + use bevy_ecs::system::SystemState; use bevy_image::Image; use bevy_math::Rect; use bevy_math::Vec2; @@ -193,20 +194,19 @@ mod tests { #[test] fn $name() { let mut app = App::new(); - let mut texture_atlas_layout_assets = Assets::::default(); + + app.add_plugins(MinimalAssetPlugin) + .init_asset::() + .init_asset::(); // Create a simple 3x3 texture atlas layout for the test cases // that use a texture atlas. In the future we could adjust the // test cases to use different texture atlas layouts. let layout = TextureAtlasLayout::from_grid(UVec2::new(3, 3), 1, 1, None, None); - let layout_handle = texture_atlas_layout_assets.add(layout); - - app.insert_resource(texture_atlas_layout_assets); + let layout_handle = app.world_mut().spawn_asset(layout); - let texture_atlases = app - .world() - .get_resource::>() - .unwrap(); + let mut state = SystemState::>::new(app.world_mut()); + let texture_atlases = state.get(app.world()).unwrap(); let image = create_image_rgba8(&[0; 3 * 3 * 4]); // 3x3 image diff --git a/crates/bevy_winit/src/cursor/mod.rs b/crates/bevy_winit/src/cursor/mod.rs index 85c56f6ff1114..c9f8959082932 100644 --- a/crates/bevy_winit/src/cursor/mod.rs +++ b/crates/bevy_winit/src/cursor/mod.rs @@ -110,8 +110,8 @@ fn update_cursors( mut commands: Commands, windows: Query<(Entity, Ref), With>, #[cfg(feature = "custom_cursor")] cursor_cache: Res, - #[cfg(feature = "custom_cursor")] images: Res>, - #[cfg(feature = "custom_cursor")] texture_atlases: Res>, + #[cfg(feature = "custom_cursor")] images: Assets, + #[cfg(feature = "custom_cursor")] texture_atlases: Assets, mut queue: Local, ) { for (entity, cursor) in windows.iter() { diff --git a/crates/bevy_world_serialization/src/lib.rs b/crates/bevy_world_serialization/src/lib.rs index f9201c04436c7..5a664f205cc6d 100644 --- a/crates/bevy_world_serialization/src/lib.rs +++ b/crates/bevy_world_serialization/src/lib.rs @@ -126,7 +126,7 @@ impl Plugin for WorldSerializationPlugin { #[cfg(test)] mod tests { use bevy_app::App; - use bevy_asset::{AssetPlugin, Assets}; + use bevy_asset::{AssetPlugin, DirectAssetAccessExt}; use bevy_ecs::{ component::Component, entity::Entity, @@ -137,8 +137,7 @@ mod tests { use bevy_reflect::Reflect; use crate::{ - DynamicWorld, DynamicWorldBuilder, DynamicWorldRoot, WorldAsset, WorldAssetRoot, - WorldSerializationPlugin, + DynamicWorldBuilder, DynamicWorldRoot, WorldAsset, WorldAssetRoot, WorldSerializationPlugin, }; #[derive(Component, Reflect, PartialEq, Debug)] @@ -177,10 +176,7 @@ mod tests { .register_type::() .register_type::(); - let handle = app - .world_mut() - .resource_mut::>() - .reserve_handle(); + let handle = app.world_mut().reserve_asset_handle(); let instance_entity = app.world_mut().spawn(WorldAssetRoot(handle.clone())).id(); app.update(); @@ -205,10 +201,7 @@ mod tests { )); world_1.world.spawn((Circle { radius: 7.0 }, ChildOf(root))); - app.world_mut() - .resource_mut::>() - .insert(&handle, world_1) - .unwrap(); + app.world_mut().insert_asset(&handle, world_1).unwrap(); app.update(); // TODO: multiple updates to avoid debounced asset events. See comment on WorldInstanceSpawner::debounced_world_asset_events @@ -263,10 +256,7 @@ mod tests { ChildOf(root), )); - app.world_mut() - .resource_mut::>() - .insert(&handle, world_2) - .unwrap(); + app.world_mut().insert_asset(&handle, world_2).unwrap(); app.update(); app.update(); @@ -309,10 +299,7 @@ mod tests { .register_type::() .register_type::(); - let handle = app - .world_mut() - .resource_mut::>() - .reserve_handle(); + let handle = app.world_mut().reserve_asset_handle(); let instance_entity = app.world_mut().spawn(DynamicWorldRoot(handle.clone())).id(); app.update(); @@ -351,8 +338,7 @@ mod tests { let dynamic_world_1 = create_dynamic_world(world_1, app.world()); app.world_mut() - .resource_mut::>() - .insert(&handle, dynamic_world_1) + .insert_asset(&handle, dynamic_world_1) .unwrap(); app.update(); @@ -411,8 +397,7 @@ mod tests { let dynamic_world_2 = create_dynamic_world(world_2, app.world()); app.world_mut() - .resource_mut::>() - .insert(&handle, dynamic_world_2) + .insert_asset(&handle, dynamic_world_2) .unwrap(); app.update(); diff --git a/crates/bevy_world_serialization/src/world_asset_spawner.rs b/crates/bevy_world_serialization/src/world_asset_spawner.rs index e4038e0e9d1ad..5cd2ec241fede 100644 --- a/crates/bevy_world_serialization/src/world_asset_spawner.rs +++ b/crates/bevy_world_serialization/src/world_asset_spawner.rs @@ -1,6 +1,7 @@ use crate::{DynamicWorld, DynamicWorldRoot, WorldAsset}; -use bevy_asset::{AssetEvent, AssetId, Assets, Handle}; +use bevy_asset::{AssetEvent, AssetId, DirectAssetAccessExt, Handle}; use bevy_ecs::{ + change_detection::DetectChangesMut, entity::{Entity, EntityHashMap}, event::EntityEvent, hierarchy::ChildOf, @@ -299,13 +300,17 @@ impl WorldInstanceSpawner { id: AssetId, entity_map: &mut EntityHashMap, ) -> Result<(), WorldInstanceSpawnError> { - world.resource_scope(|world, dynamic_worlds: Mut>| { - let dynamic_world = dynamic_worlds - .get(id) - .ok_or(WorldInstanceSpawnError::NonExistentDynamicWorld { id })?; - - dynamic_world.write_to_world(world, entity_map) - }) + let mut dynamic_scene = world + .get_asset_mut(id) + .ok_or(WorldInstanceSpawnError::NonExistentDynamicWorld { id })?; + + // Hokey-pokey the dynamic scene out of the world. + let scene_to_spawn = core::mem::take(dynamic_scene.bypass_change_detection()); + scene_to_spawn.write_to_world(world, entity_map)?; + let mut dynamic_scene = world.get_asset_mut(id).unwrap(); + // Put the scene data back into the asset. + *dynamic_scene.bypass_change_detection() = scene_to_spawn; + Ok(()) } /// Immediately spawns a new instance of the provided world asset. @@ -338,17 +343,22 @@ impl WorldInstanceSpawner { id: AssetId, entity_map: &mut EntityHashMap, ) -> Result<(), WorldInstanceSpawnError> { - world.resource_scope(|world, world_assets: Mut>| { - let world_asset = world_assets - .get(id) - .ok_or(WorldInstanceSpawnError::NonExistentWorldAsset { id })?; + let type_registry = world.resource::().clone(); - world_asset.write_to_world_with( - world, - entity_map, - &world.resource::().clone(), - ) - }) + let mut scene = world + .get_asset_mut(id) + .ok_or(WorldInstanceSpawnError::NonExistentWorldAsset { id })?; + + // Hokey-pokey the scene out of the world. + let scene_to_spawn = core::mem::replace( + scene.bypass_change_detection(), + WorldAsset::new(World::new()), + ); + scene_to_spawn.write_to_world_with(world, entity_map, &type_registry)?; + let mut scene = world.get_asset_mut(id).unwrap(); + // Put the scene data back into the asset. + *scene.bypass_change_detection() = scene_to_spawn; + Ok(()) } /// Iterate through all instances of the provided worlds and update those immediately. @@ -721,7 +731,7 @@ pub fn world_instance_spawner( #[cfg(test)] mod tests { use bevy_app::App; - use bevy_asset::{AssetPlugin, AssetServer, Handle}; + use bevy_asset::{AssetPlugin, AssetServer, Handle, MinimalAssetPlugin}; use bevy_ecs::{ component::Component, hierarchy::Children, @@ -737,7 +747,6 @@ mod tests { use super::*; use crate::{DynamicWorld, WorldInstanceSpawner}; use bevy_app::ScheduleRunnerPlugin; - use bevy_asset::Assets; use bevy_ecs::{ entity::Entity, prelude::{AppTypeRegistry, World}, @@ -768,10 +777,7 @@ mod tests { &world, &app.world().resource::().read(), ); - let dynamic_world_handle = app - .world_mut() - .resource_mut::>() - .add(dynamic_world); + let dynamic_world_handle = app.world_mut().spawn_asset(dynamic_world); // spawn the world as a child of `entity` using `DynamicWorldRoot` let entity = app @@ -815,41 +821,40 @@ mod tests { #[test] fn clone_dynamic_entities() { - let mut world = World::default(); + let mut app = App::new(); + app.add_plugins(MinimalAssetPlugin); // setup let atr = AppTypeRegistry::default(); atr.write().register::(); - world.insert_resource(atr); - world.insert_resource(Assets::::default()); + app.world_mut().insert_resource(atr); // start test - world.spawn(A(42)); + app.world_mut().spawn(A(42)); - assert_eq!(world.query::<&A>().iter(&world).len(), 1); + assert_eq!(app.world_mut().query::<&A>().iter(app.world()).len(), 1); // clone only existing entity let mut world_asset_spawner = WorldInstanceSpawner::default(); - let entity = world + let entity = app + .world_mut() .query_filtered::>() - .single(&world) + .single(app.world()) .unwrap(); let dynamic_world = { - let type_registry = world.resource::().read(); - DynamicWorldBuilder::from_world(&world, &type_registry) + let type_registry = app.world().resource::().read(); + DynamicWorldBuilder::from_world(app.world(), &type_registry) .extract_entity(entity) .build() }; - let dynamic_world_id = world - .resource_mut::>() - .add(dynamic_world); + let dynamic_world_id = app.world_mut().spawn_asset(dynamic_world); let instance_id = world_asset_spawner - .spawn_dynamic_sync(&mut world, &dynamic_world_id) + .spawn_dynamic_sync(app.world_mut(), &dynamic_world_id) .unwrap(); // verify we spawned exactly one new entity with our expected component - assert_eq!(world.query::<&A>().iter(&world).len(), 2); + assert_eq!(app.world_mut().query::<&A>().iter(app.world()).len(), 2); // verify that we can get this newly-spawned entity by the instance ID let new_entity = world_asset_spawner @@ -861,9 +866,10 @@ mod tests { assert_ne!(entity, new_entity); // verify this new entity contains the same data as the original entity - let [old_a, new_a] = world + let [old_a, new_a] = app + .world_mut() .query::<&A>() - .get_many(&world, [entity, new_entity]) + .get_many(app.world(), [entity, new_entity]) .unwrap(); assert_eq!(old_a, new_a); } @@ -1124,10 +1130,7 @@ mod tests { .add_children(&[child0, child1, child2]); let world_asset = WorldAsset::new(asset_world); - let handle = app - .world_mut() - .resource_mut::>() - .add(world_asset); + let handle = app.world_mut().spawn_asset(world_asset); let spawned = app.world_mut().spawn(WorldAssetRoot(handle.clone())).id(); diff --git a/errors/B0002.md b/errors/B0002.md index 21a92cbc15171..53abe05d4fe52 100644 --- a/errors/B0002.md +++ b/errors/B0002.md @@ -7,9 +7,9 @@ Erroneous code example: ```rust,should_panic use bevy::prelude::*; -fn update_materials( - mut material_updater: ResMut>, - current_materials: Res>, +fn play_with_time( + mut time_ticker: ResMut::add`] on the shape, which works because the [`Assets::add`] method takes anything that can be turned into the asset type it stores. There's an implementation for [`From`] on shape primitives into [`Mesh`], so that will get called internally by [`Assets::add`]. +//! While a shape is not a mesh, turning it into one in Bevy is easy. In this example we call [`asset_commands.spawn_asset(/* Shape here! */)`] and [`Mesh::from`]. //! //! [`Extrusion`] lets us turn 2D shape primitives into versions of those shapes that have volume by extruding them. A 1x1 square that gets wrapped in this with an extrusion depth of 2 will give us a rectangular prism of size 1x1x2, but here we're just extruding these 2d shapes by depth 1. //! @@ -54,44 +54,40 @@ const EXTRUSION_X_EXTENT: f32 = 14.0; const Z_EXTENT: f32 = 8.0; const THICKNESS: f32 = 0.1; -fn setup( - mut commands: Commands, - mut meshes: ResMut>, - mut images: ResMut>, - mut materials: ResMut>, -) { - let debug_material = materials.add(StandardMaterial { - base_color_texture: Some(images.add(uv_debug_texture())), +fn setup(mut commands: Commands, mut asset_commands: AssetCommands) { + let debug_texture = asset_commands.spawn_asset(uv_debug_texture()); + let debug_material = asset_commands.spawn_asset(StandardMaterial { + base_color_texture: Some(debug_texture), ..default() }); let shapes = [ - meshes.add(Cuboid::default()), - meshes.add(Tetrahedron::default()), - meshes.add(Capsule3d::default()), - meshes.add(Torus::default()), - meshes.add(Cylinder::default()), - meshes.add(Cone::default()), - meshes.add(ConicalFrustum::default()), - meshes.add(Sphere::default().mesh().ico(5).unwrap()), - meshes.add(Sphere::default().mesh().uv(32, 18)), - meshes.add(Segment3d::default()), - meshes.add(Polyline3d::new(vec![ + asset_commands.spawn_asset(Mesh::from(Cuboid::default())), + asset_commands.spawn_asset(Mesh::from(Tetrahedron::default())), + asset_commands.spawn_asset(Mesh::from(Capsule3d::default())), + asset_commands.spawn_asset(Mesh::from(Torus::default())), + asset_commands.spawn_asset(Mesh::from(Cylinder::default())), + asset_commands.spawn_asset(Mesh::from(Cone::default())), + asset_commands.spawn_asset(Mesh::from(ConicalFrustum::default())), + asset_commands.spawn_asset(Sphere::default().mesh().ico(5).unwrap()), + asset_commands.spawn_asset(Sphere::default().mesh().uv(32, 18)), + asset_commands.spawn_asset(Mesh::from(Segment3d::default())), + asset_commands.spawn_asset(Mesh::from(Polyline3d::new(vec![ Vec3::new(-0.5, 0.0, 0.0), Vec3::new(0.5, 0.0, 0.0), Vec3::new(0.0, 0.5, 0.0), - ])), + ]))), ]; let extrusions = [ - meshes.add(Extrusion::new(Rectangle::default(), 1.)), - meshes.add(Extrusion::new(Capsule2d::default(), 1.)), - meshes.add(Extrusion::new(Annulus::default(), 1.)), - meshes.add(Extrusion::new(Circle::default(), 1.)), - meshes.add(Extrusion::new(Ellipse::default(), 1.)), - meshes.add(Extrusion::new(RegularPolygon::default(), 1.)), - meshes.add(Extrusion::new(Triangle2d::default(), 1.)), - meshes.add(Extrusion::new( + asset_commands.spawn_asset(Mesh::from(Extrusion::new(Rectangle::default(), 1.))), + asset_commands.spawn_asset(Mesh::from(Extrusion::new(Capsule2d::default(), 1.))), + asset_commands.spawn_asset(Mesh::from(Extrusion::new(Annulus::default(), 1.))), + asset_commands.spawn_asset(Mesh::from(Extrusion::new(Circle::default(), 1.))), + asset_commands.spawn_asset(Mesh::from(Extrusion::new(Ellipse::default(), 1.))), + asset_commands.spawn_asset(Mesh::from(Extrusion::new(RegularPolygon::default(), 1.))), + asset_commands.spawn_asset(Mesh::from(Extrusion::new(Triangle2d::default(), 1.))), + asset_commands.spawn_asset(Mesh::from(Extrusion::new( ConvexPolygon::new(vec![ Vec2::new(0.0, 0.8), Vec2::new(-0.47, 0.25), @@ -101,18 +97,27 @@ fn setup( ]) .unwrap(), 1.0, - )), + ))), ]; let ring_extrusions = [ - meshes.add(Extrusion::new(Rectangle::default().to_ring(THICKNESS), 1.)), - meshes.add(Extrusion::new(Capsule2d::default().to_ring(THICKNESS), 1.)), - meshes.add(Extrusion::new( + asset_commands.spawn_asset(Mesh::from(Extrusion::new( + Rectangle::default().to_ring(THICKNESS), + 1., + ))), + asset_commands.spawn_asset(Mesh::from(Extrusion::new( + Capsule2d::default().to_ring(THICKNESS), + 1., + ))), + asset_commands.spawn_asset(Mesh::from(Extrusion::new( Ring::new(Circle::new(1.0), Circle::new(0.5)), 1., - )), - meshes.add(Extrusion::new(Circle::default().to_ring(THICKNESS), 1.)), - meshes.add(Extrusion::new( + ))), + asset_commands.spawn_asset(Mesh::from(Extrusion::new( + Circle::default().to_ring(THICKNESS), + 1., + ))), + asset_commands.spawn_asset(Mesh::from(Extrusion::new( { // This is an approximation; Ellipse does not implement Inset as concentric ellipses do not have parallel curves let outer = Ellipse::default(); @@ -121,12 +126,15 @@ fn setup( Ring::new(outer, inner) }, 1., - )), - meshes.add(Extrusion::new( + ))), + asset_commands.spawn_asset(Mesh::from(Extrusion::new( RegularPolygon::default().to_ring(THICKNESS), 1., - )), - meshes.add(Extrusion::new(Triangle2d::default().to_ring(THICKNESS), 1.)), + ))), + asset_commands.spawn_asset(Mesh::from(Extrusion::new( + Triangle2d::default().to_ring(THICKNESS), + 1., + ))), ]; let num_shapes = shapes.len(); @@ -195,8 +203,10 @@ fn setup( // ground plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(50.0, 50.0).subdivisions(10))), - MeshMaterial3d(materials.add(Color::from(SILVER))), + Mesh3d(asset_commands.spawn_asset(Mesh::from( + Plane3d::default().mesh().size(50.0, 50.0).subdivisions(10), + ))), + MeshMaterial3d(asset_commands.spawn_asset(StandardMaterial::from(Color::from(SILVER)))), )); commands.spawn(( diff --git a/examples/3d/3d_viewport_to_world.rs b/examples/3d/3d_viewport_to_world.rs index 231306bd2b6dc..5fdb805c90085 100644 --- a/examples/3d/3d_viewport_to_world.rs +++ b/examples/3d/3d_viewport_to_world.rs @@ -39,15 +39,13 @@ fn draw_cursor( #[derive(Component)] struct Ground; -fn setup( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { +fn setup(mut commands: Commands, mut asset_commands: AssetCommands) { // plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(20., 20.))), - MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), + Mesh3d(asset_commands.spawn_asset(Mesh::from(Plane3d::default().mesh().size(20., 20.)))), + MeshMaterial3d( + asset_commands.spawn_asset(StandardMaterial::from(Color::srgb(0.3, 0.5, 0.3))), + ), Ground, )); diff --git a/examples/3d/animated_material.rs b/examples/3d/animated_material.rs index 820b18365a434..1bf99015cd55a 100644 --- a/examples/3d/animated_material.rs +++ b/examples/3d/animated_material.rs @@ -12,9 +12,8 @@ fn main() { fn setup( mut commands: Commands, + mut asset_commands: AssetCommands, asset_server: Res, - mut meshes: ResMut>, - mut materials: ResMut>, ) { commands.spawn(( Camera3d::default(), @@ -27,7 +26,7 @@ fn setup( }, )); - let cube = meshes.add(Cuboid::new(0.5, 0.5, 0.5)); + let cube = asset_commands.spawn_asset(Mesh::from(Cuboid::new(0.5, 0.5, 0.5))); const GOLDEN_ANGLE: f32 = 137.507_77; @@ -36,7 +35,9 @@ fn setup( for z in -1..2 { commands.spawn(( Mesh3d(cube.clone()), - MeshMaterial3d(materials.add(Color::from(hsla))), + MeshMaterial3d( + asset_commands.spawn_asset(StandardMaterial::from(Color::from(hsla))), + ), Transform::from_translation(Vec3::new(x as f32, 0.0, z as f32)), )); hsla = hsla.rotate_hue(GOLDEN_ANGLE); @@ -47,7 +48,7 @@ fn setup( fn animate_materials( material_handles: Query<&MeshMaterial3d>, time: Res)>, ) { for (entity, Compressed { compressed, .. }) in query.iter() { - let Some(GzAsset { uncompressed }) = compressed_assets.remove(compressed) else { + if compressed_assets.get(compressed).is_none() { continue; - }; - - let uncompressed = uncompressed.take::().unwrap(); - - commands - .entity(entity) - .remove::>() - .insert(T::from(asset_server.add(uncompressed))); + } + let asset_server = asset_server.clone(); + let compressed = compressed.clone(); + commands.queue(move |world: &mut World| { + let GzAsset { uncompressed } = world.remove_asset(compressed.id()).unwrap(); + + let uncompressed = uncompressed.take::().unwrap(); + + world + .entity_mut(entity) + .remove::>() + .insert(T::from(asset_server.add(uncompressed))); + }); } } diff --git a/examples/asset/asset_loading.rs b/examples/asset/asset_loading.rs index 49d2dca279025..aa725cb9d3cb0 100644 --- a/examples/asset/asset_loading.rs +++ b/examples/asset/asset_loading.rs @@ -11,9 +11,9 @@ fn main() { fn setup( mut commands: Commands, + mut asset_commands: AssetCommands, asset_server: Res, - meshes: Res>, - mut materials: ResMut>, + meshes: Assets, ) { // By default AssetServer will load assets from inside the "assets" folder. // For example, the next line will load GltfAssetLabel::Primitive{mesh:0,primitive:0}.from_asset("ROOT/assets/models/cube/cube.gltf"), @@ -67,7 +67,7 @@ fn setup( ); // You can also add assets directly to their Assets storage: - let material_handle = materials.add(StandardMaterial { + let material_handle = asset_commands.spawn_asset(StandardMaterial { base_color: Color::srgb(0.8, 0.7, 0.6), ..default() }); diff --git a/examples/asset/asset_saving.rs b/examples/asset/asset_saving.rs index e005911cdca30..2290f3fa3701c 100644 --- a/examples/asset/asset_saving.rs +++ b/examples/asset/asset_saving.rs @@ -37,7 +37,7 @@ const ASSET_PATH: &str = "art_project.png"; fn perform_save( image_to_save: Res, - images: Res>, + images: Assets, asset_server: Res, ) { let image = images.get(&image_to_save.0).unwrap(); @@ -84,8 +84,8 @@ struct SpriteToSave; fn setup( mut commands: Commands, + mut asset_commands: AssetCommands, asset_server: Res, - mut images: ResMut>, ) { commands.spawn(( Camera2d, @@ -123,23 +123,21 @@ F5 - Save image" // image into that handle. If the load succeeds, the image will be replaced with the loaded // contents. If it fails, the default image will remain. In real code, you likely want to poll // `AssetServer::load_state` and only insert this on load failure. - images - .insert(&handle, { - let mut image = Image::new_fill( - Extent3d { - width: 100, - height: 100, - depth_or_array_layers: 1, - }, - TextureDimension::D2, - &[0, 0, 0, 255], - TextureFormat::Rgba8Unorm, - RenderAssetUsages::all(), - ); - image.sampler = ImageSampler::nearest(); - image - }) - .unwrap(); + asset_commands.insert_asset(&handle, { + let mut image = Image::new_fill( + Extent3d { + width: 100, + height: 100, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + &[0, 0, 0, 255], + TextureFormat::Rgba8Unorm, + RenderAssetUsages::all(), + ); + image.sampler = ImageSampler::nearest(); + image + }); commands.insert_resource(ImageToSave(handle)); @@ -209,9 +207,9 @@ fn try_plot( event: On, sprite: Query<(&Sprite, &Anchor, &GlobalTransform), With>, camera: Single<(&Camera, &GlobalTransform)>, - texture_atlases: Res>, + texture_atlases: Assets, draw_color: Res, - mut images: ResMut>, + mut images: AssetsMut, ) { let Ok((sprite, anchor, sprite_transform)) = sprite.get(event.entity) else { return; @@ -228,7 +226,7 @@ fn try_plot( let Ok(pixel_space) = sprite.compute_pixel_space_point( relative_to_sprite.xy(), *anchor, - &images, + &images.as_readonly(), &texture_atlases, ) else { return; diff --git a/examples/asset/asset_saving_with_subassets.rs b/examples/asset/asset_saving_with_subassets.rs index 267327ed1020a..87cebf8ba9895 100644 --- a/examples/asset/asset_saving_with_subassets.rs +++ b/examples/asset/asset_saving_with_subassets.rs @@ -100,8 +100,8 @@ struct PendingLoad(Handle); /// Waits for any [`PendingLoad`]s to complete, and spawns in their boxes when they do. fn wait_for_pending_loads( loads: Populated<(Entity, &PendingLoad)>, - many_boxes: Res>, - one_boxes: Res>, + many_boxes: Assets, + one_boxes: Assets, existing_boxes: Query>, mut commands: Commands, ) { diff --git a/examples/asset/custom_asset.rs b/examples/asset/custom_asset.rs index 60982e53fa313..2d570bd2a65ec 100644 --- a/examples/asset/custom_asset.rs +++ b/examples/asset/custom_asset.rs @@ -123,8 +123,8 @@ fn setup(mut state: ResMut, asset_server: Res) { fn print_on_load( mut state: ResMut, - custom_assets: Res>, - blob_assets: Res>, + custom_assets: Assets, + blob_assets: Assets, ) { let custom_asset = custom_assets.get(&state.handle); let other_custom_asset = custom_assets.get(&state.other_handle); diff --git a/examples/asset/generated_assets.rs b/examples/asset/generated_assets.rs index 22f4f0eb7a987..71072c6069a5e 100644 --- a/examples/asset/generated_assets.rs +++ b/examples/asset/generated_assets.rs @@ -12,9 +12,8 @@ fn main() { fn setup( mut commands: Commands, + mut asset_commands: AssetCommands, asset_server: Res, - mut materials: ResMut>, - meshes: Res>, ) { commands.spawn((Camera3d::default(), Transform::from_xyz(0.0, 0.0, 5.0))); @@ -23,22 +22,22 @@ fn setup( Transform::default().looking_to(Dir3::new(Vec3::new(-1.0, -1.0, -1.0)).unwrap(), Dir3::Y), )); - // The simplest way to generate an asset is to add it directly to the `Assets`. - let material_handle = materials.add(StandardMaterial::default()); + // The simplest way to generate an asset is to spawn it directly with `AssetCommands`. + let material_handle = asset_commands.spawn_asset(StandardMaterial::default()); commands.spawn(( Transform::from_xyz(-2.0, 0.0, 0.0), MeshMaterial3d(material_handle.clone()), // Alternatively, `add_async` creates a task that runs your async function. Once it - // completes, the asset is added to the `Assets`. This is "deferred" meaning that the asset - // may take a frame to be added after the task completes. + // completes, the asset is inserted into the returned handle. This is "deferred" meaning + // that the asset may take a frame to be added after the task completes. Mesh3d(asset_server.add_async(generate_mesh_async())), )); - // The last way to generate assets is to reserve a handle, and then use `Assets::insert` to - // populate the asset later. In this example, the `generate_mesh_system` system runs to populate - // the mesh. - let mesh_handle = meshes.reserve_handle(); + // The last way to generate assets is to reserve a handle, and then use + // `AssetCommands::insert_asset` to populate the asset later. In this example, the + // `generate_mesh_system` system runs to populate the mesh. + let mesh_handle = asset_commands.reserve_handle(); commands.insert_resource(HandleToGenerate(mesh_handle.clone())); commands.spawn(( Transform::from_xyz(2.0, 0.0, 0.0) @@ -63,9 +62,9 @@ struct HandleToGenerate(Handle); /// This generates a runtime mesh. Since it's a system, it can use other data in the world to /// generate the asset! fn generate_mesh_system( + mut asset_commands: AssetCommands, handle_to_generate: Res, - mut meshes: ResMut>, ) { let mesh = Mesh::from(Torus::new(0.8, 1.2)); - meshes.insert(&handle_to_generate.0, mesh).unwrap(); + asset_commands.insert_asset(&handle_to_generate.0, mesh); } diff --git a/examples/asset/multi_asset_sync.rs b/examples/asset/multi_asset_sync.rs index 1e52d0061b823..8dbef527df72e 100644 --- a/examples/asset/multi_asset_sync.rs +++ b/examples/asset/multi_asset_sync.rs @@ -185,11 +185,7 @@ fn setup_ui(mut commands: Commands) { )); } -fn setup_scene( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { +fn setup_scene(mut commands: Commands, mut asset_commands: AssetCommands) { // Camera commands.spawn(( Camera3d::default(), @@ -207,8 +203,13 @@ fn setup_scene( // Plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(50000.0, 50000.0))), - MeshMaterial3d(materials.add(Color::srgb(0.7, 0.2, 0.2))), + Mesh3d( + asset_commands + .spawn_asset(Mesh::from(Plane3d::default().mesh().size(50000.0, 50000.0))), + ), + MeshMaterial3d( + asset_commands.spawn_asset(StandardMaterial::from(Color::srgb(0.7, 0.2, 0.2))), + ), Loading, )); } @@ -225,14 +226,18 @@ fn assets_loaded(barrier: Option>) -> bool { fn wait_on_load( mut commands: Commands, foxes: Res, - gltfs: Res>, - mut meshes: ResMut>, - mut materials: ResMut>, + gltfs: Assets, + mut asset_commands: AssetCommands, ) { // Change color of plane to green commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(50000.0, 50000.0))), - MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), + Mesh3d( + asset_commands + .spawn_asset(Mesh::from(Plane3d::default().mesh().size(50000.0, 50000.0))), + ), + MeshMaterial3d( + asset_commands.spawn_asset(StandardMaterial::from(Color::srgb(0.3, 0.5, 0.3))), + ), Transform::from_translation(Vec3::Z * -0.01), )); diff --git a/examples/asset/processing/asset_processing.rs b/examples/asset/processing/asset_processing.rs index 0c2647f137c87..d9148efb77b9b 100644 --- a/examples/asset/processing/asset_processing.rs +++ b/examples/asset/processing/asset_processing.rs @@ -248,7 +248,7 @@ fn setup(mut commands: Commands, assets: Res) { fn print_text( handles: Res, - texts: Res>, + texts: Assets, mut asset_events: MessageReader>, ) { if !asset_events.is_empty() { diff --git a/examples/asset/repeated_texture.rs b/examples/asset/repeated_texture.rs index 55ea9abe94f08..2f960d6590c7a 100644 --- a/examples/asset/repeated_texture.rs +++ b/examples/asset/repeated_texture.rs @@ -17,16 +17,15 @@ fn main() { fn setup( mut commands: Commands, asset_server: Res, - mut meshes: ResMut>, - mut materials: ResMut>, + mut asset_commands: AssetCommands, ) { let image_with_default_sampler = asset_server.load("textures/fantasy_ui_borders/panel-border-010.png"); // central cube with not repeated texture commands.spawn(( - Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), - MeshMaterial3d(materials.add(StandardMaterial { + Mesh3d(asset_commands.spawn_asset(Mesh::from(Cuboid::new(1.0, 1.0, 1.0)))), + MeshMaterial3d(asset_commands.spawn_asset(StandardMaterial { base_color_texture: Some(image_with_default_sampler.clone()), ..default() })), @@ -35,9 +34,9 @@ fn setup( // left cube with repeated texture commands.spawn(( - Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), + Mesh3d(asset_commands.spawn_asset(Mesh::from(Cuboid::new(1.0, 1.0, 1.0)))), MeshMaterial3d( - materials.add(StandardMaterial { + asset_commands.spawn_asset(StandardMaterial { base_color_texture: Some( asset_server .load_builder() @@ -66,8 +65,8 @@ fn setup( // right cube with scaled texture, because with default sampler commands.spawn(( - Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), - MeshMaterial3d(materials.add(StandardMaterial { + Mesh3d(asset_commands.spawn_asset(Mesh::from(Cuboid::new(1.0, 1.0, 1.0)))), + MeshMaterial3d(asset_commands.spawn_asset(StandardMaterial { // there is no sampler set, that's why // by default you see only one small image in a row/column // and other space is filled by image edge diff --git a/examples/async_tasks/async_channel_pattern.rs b/examples/async_tasks/async_channel_pattern.rs index 24e82eb162cdf..8f9b4f220920c 100644 --- a/examples/async_tasks/async_channel_pattern.rs +++ b/examples/async_tasks/async_channel_pattern.rs @@ -113,30 +113,23 @@ struct BoxMeshHandle(Handle); struct BoxMaterialHandle(Handle); /// Sets up the shared mesh and material for the cubes. -fn setup_assets( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { +fn setup_assets(mut commands: Commands, mut asset_commands: AssetCommands) { // Create and store a cube mesh - let box_mesh_handle = meshes.add(Cuboid::new(0.4, 0.4, 0.4)); + let box_mesh_handle = asset_commands.spawn_asset(Mesh::from(Cuboid::new(0.4, 0.4, 0.4))); commands.insert_resource(BoxMeshHandle(box_mesh_handle)); // Create and store a red material - let box_material_handle = materials.add(Color::srgb(1.0, 0.2, 0.3)); + let box_material_handle = + asset_commands.spawn_asset(StandardMaterial::from(Color::srgb(1.0, 0.2, 0.3))); commands.insert_resource(BoxMaterialHandle(box_material_handle)); } /// Sets up the environment by spawning the ground, light, and camera. -fn setup_env( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { +fn setup_env(mut commands: Commands, mut asset_commands: AssetCommands) { // Spawn a circular ground plane commands.spawn(( - Mesh3d(meshes.add(Circle::new(1.618 * NUM_CUBES as f32))), - MeshMaterial3d(materials.add(Color::WHITE)), + Mesh3d(asset_commands.spawn_asset(Mesh::from(Circle::new(1.618 * NUM_CUBES as f32)))), + MeshMaterial3d(asset_commands.spawn_asset(StandardMaterial::from(Color::WHITE))), Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), )); diff --git a/examples/async_tasks/async_compute.rs b/examples/async_tasks/async_compute.rs index 7fab1c7e136bf..9b6a605d2800e 100644 --- a/examples/async_tasks/async_compute.rs +++ b/examples/async_tasks/async_compute.rs @@ -41,15 +41,12 @@ struct BoxMaterialHandle(Handle); /// and Box Material assets, adds them to their respective Asset /// Resources, and stores their handles as resources so we can access /// them later when we're ready to render our Boxes -fn add_assets( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { - let box_mesh_handle = meshes.add(Cuboid::new(0.25, 0.25, 0.25)); +fn add_assets(mut commands: Commands, mut asset_commands: AssetCommands) { + let box_mesh_handle = asset_commands.spawn_asset(Mesh::from(Cuboid::new(0.25, 0.25, 0.25))); commands.insert_resource(BoxMeshHandle(box_mesh_handle)); - let box_material_handle = materials.add(Color::srgb(1.0, 0.2, 0.3)); + let box_material_handle = + asset_commands.spawn_asset(StandardMaterial::from(Color::srgb(1.0, 0.2, 0.3))); commands.insert_resource(BoxMaterialHandle(box_material_handle)); } diff --git a/examples/audio/decodable.rs b/examples/audio/decodable.rs index 8f7e67ad14dfa..7e4fee737f170 100644 --- a/examples/audio/decodable.rs +++ b/examples/audio/decodable.rs @@ -92,9 +92,9 @@ fn main() { .run(); } -fn setup(mut assets: ResMut>, mut commands: Commands) { +fn setup(mut commands: Commands, mut asset_commands: AssetCommands) { // add a `SineAudio` to the asset server so that it can be played - let audio_handle = assets.add(SineAudio { + let audio_handle = asset_commands.spawn_asset(SineAudio { frequency: 440., // this is the frequency of A4 }); commands.spawn(AudioPlayer(audio_handle)); diff --git a/examples/audio/pitch.rs b/examples/audio/pitch.rs index c7d30c251de4b..4cd02386b8b37 100644 --- a/examples/audio/pitch.rs +++ b/examples/audio/pitch.rs @@ -23,7 +23,8 @@ fn setup(mut commands: Commands) { } fn play_pitch( - mut pitch_assets: ResMut>, + mut asset_commands: AssetCommands, + pitch_assets: Assets, frequency: Res, mut play_pitch_reader: MessageReader, mut commands: Commands, @@ -31,10 +32,10 @@ fn play_pitch( for _ in play_pitch_reader.read() { info!("playing pitch with frequency: {}", frequency.0); commands.spawn(( - AudioPlayer(pitch_assets.add(Pitch::new(frequency.0, Duration::new(1, 0)))), + AudioPlayer(asset_commands.spawn_asset(Pitch::new(frequency.0, Duration::new(1, 0)))), PlaybackSettings::DESPAWN, )); - info!("number of pitch assets: {}", pitch_assets.len()); + info!("number of pitch assets: {}", pitch_assets.count()); } } diff --git a/examples/audio/spatial_audio_2d.rs b/examples/audio/spatial_audio_2d.rs index cffd0498242c5..21f8b8d8df875 100644 --- a/examples/audio/spatial_audio_2d.rs +++ b/examples/audio/spatial_audio_2d.rs @@ -25,8 +25,7 @@ fn main() { fn setup( mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, + mut asset_commands: AssetCommands, asset_server: Res, ) { // Space between the two ears @@ -34,8 +33,8 @@ fn setup( // sound emitter commands.spawn(( - Mesh2d(meshes.add(Circle::new(15.0))), - MeshMaterial2d(materials.add(Color::from(BLUE))), + Mesh2d(asset_commands.spawn_asset(Mesh::from(Circle::new(15.0)))), + MeshMaterial2d(asset_commands.spawn_asset(ColorMaterial::from(Color::from(BLUE)))), Transform::from_translation(Vec3::new(0.0, 50.0, 0.0)), Emitter::default(), AudioPlayer::new(asset_server.load("sounds/Windless Slopes.ogg")), diff --git a/examples/audio/spatial_audio_3d.rs b/examples/audio/spatial_audio_3d.rs index b921fe6fcfd10..dffd0427f551e 100644 --- a/examples/audio/spatial_audio_3d.rs +++ b/examples/audio/spatial_audio_3d.rs @@ -18,16 +18,15 @@ fn main() { fn setup( mut commands: Commands, asset_server: Res, - mut meshes: ResMut>, - mut materials: ResMut>, + mut asset_commands: AssetCommands, ) { // Space between the two ears let gap = 4.0; // sound emitter commands.spawn(( - Mesh3d(meshes.add(Sphere::new(0.2).mesh().uv(32, 18))), - MeshMaterial3d(materials.add(Color::from(BLUE))), + Mesh3d(asset_commands.spawn_asset(Sphere::new(0.2).mesh().uv(32, 18))), + MeshMaterial3d(asset_commands.spawn_asset(StandardMaterial::from(Color::from(BLUE)))), Transform::from_xyz(0.0, 0.0, 0.0), Emitter::default(), AudioPlayer::new(asset_server.load("sounds/Windless Slopes.ogg")), @@ -42,14 +41,18 @@ fn setup( children![ // left ear indicator ( - Mesh3d(meshes.add(Cuboid::new(0.2, 0.2, 0.2))), - MeshMaterial3d(materials.add(Color::from(RED))), + Mesh3d(asset_commands.spawn_asset(Mesh::from(Cuboid::new(0.2, 0.2, 0.2)))), + MeshMaterial3d( + asset_commands.spawn_asset(StandardMaterial::from(Color::from(RED))) + ), Transform::from_translation(listener.left_ear_offset), ), // right ear indicator ( - Mesh3d(meshes.add(Cuboid::new(0.2, 0.2, 0.2))), - MeshMaterial3d(materials.add(Color::from(LIME))), + Mesh3d(asset_commands.spawn_asset(Mesh::from(Cuboid::new(0.2, 0.2, 0.2)))), + MeshMaterial3d( + asset_commands.spawn_asset(StandardMaterial::from(Color::from(LIME))) + ), Transform::from_translation(listener.right_ear_offset), ) ], diff --git a/examples/camera/2d_screen_shake.rs b/examples/camera/2d_screen_shake.rs index f7aa6c2574e54..39bd6bcf7dde0 100644 --- a/examples/camera/2d_screen_shake.rs +++ b/examples/camera/2d_screen_shake.rs @@ -176,34 +176,34 @@ fn setup_camera(mut commands: Commands) { } /// Spawn a scene so we have something to look at. -fn setup_scene( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { +fn setup_scene(mut commands: Commands, mut asset_commands: AssetCommands) { // Background tile commands.spawn(( - Mesh2d(meshes.add(Rectangle::new(1000., 700.))), - MeshMaterial2d(materials.add(Color::srgb(0.2, 0.2, 0.3))), + Mesh2d(asset_commands.spawn_asset(Mesh::from(Rectangle::new(1000., 700.)))), + MeshMaterial2d(asset_commands.spawn_asset(ColorMaterial::from(Color::srgb(0.2, 0.2, 0.3)))), )); // The shape in the middle could be our player character. commands.spawn(( - Mesh2d(meshes.add(Rectangle::new(50.0, 100.0))), - MeshMaterial2d(materials.add(Color::srgb(0.25, 0.94, 0.91))), + Mesh2d(asset_commands.spawn_asset(Mesh::from(Rectangle::new(50.0, 100.0)))), + MeshMaterial2d( + asset_commands.spawn_asset(ColorMaterial::from(Color::srgb(0.25, 0.94, 0.91))), + ), Transform::from_xyz(0., 0., 2.), )); // These two shapes could be obstacles. commands.spawn(( - Mesh2d(meshes.add(Rectangle::new(50.0, 50.0))), - MeshMaterial2d(materials.add(Color::srgb(0.85, 0.0, 0.2))), + Mesh2d(asset_commands.spawn_asset(Mesh::from(Rectangle::new(50.0, 50.0)))), + MeshMaterial2d( + asset_commands.spawn_asset(ColorMaterial::from(Color::srgb(0.85, 0.0, 0.2))), + ), Transform::from_xyz(-450.0, 200.0, 2.), )); commands.spawn(( - Mesh2d(meshes.add(Rectangle::new(70.0, 50.0))), - MeshMaterial2d(materials.add(Color::srgb(0.5, 0.8, 0.2))), + Mesh2d(asset_commands.spawn_asset(Mesh::from(Rectangle::new(70.0, 50.0)))), + MeshMaterial2d(asset_commands.spawn_asset(ColorMaterial::from(Color::srgb(0.5, 0.8, 0.2)))), Transform::from_xyz(450.0, -150.0, 2.), )); } diff --git a/examples/camera/2d_top_down_camera.rs b/examples/camera/2d_top_down_camera.rs index 59744615f9bc1..f3b6a56c2c808 100644 --- a/examples/camera/2d_top_down_camera.rs +++ b/examples/camera/2d_top_down_camera.rs @@ -28,22 +28,20 @@ fn main() { .run(); } -fn setup_scene( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { +fn setup_scene(mut commands: Commands, mut asset_commands: AssetCommands) { // World where we move the player commands.spawn(( - Mesh2d(meshes.add(Rectangle::new(1000., 700.))), - MeshMaterial2d(materials.add(Color::srgb(0.2, 0.2, 0.3))), + Mesh2d(asset_commands.spawn_asset(Mesh::from(Rectangle::new(1000., 700.)))), + MeshMaterial2d(asset_commands.spawn_asset(ColorMaterial::from(Color::srgb(0.2, 0.2, 0.3)))), )); // Player commands.spawn(( Player, - Mesh2d(meshes.add(Circle::new(25.))), - MeshMaterial2d(materials.add(Color::srgb(6.25, 9.4, 9.1))), // RGB values exceed 1 to achieve a bright color for the bloom effect + Mesh2d(asset_commands.spawn_asset(Mesh::from(Circle::new(25.)))), + MeshMaterial2d( + asset_commands.spawn_asset(ColorMaterial::from(Color::srgb(6.25, 9.4, 9.1))), + ), // RGB values exceed 1 to achieve a bright color for the bloom effect Transform::from_xyz(0., 0., 2.), )); } diff --git a/examples/camera/camera_orbit.rs b/examples/camera/camera_orbit.rs index a4d2fafd3001d..f7bc15cf518b1 100644 --- a/examples/camera/camera_orbit.rs +++ b/examples/camera/camera_orbit.rs @@ -43,11 +43,7 @@ fn main() { } /// Set up a simple 3D scene -fn setup( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { +fn setup(mut commands: Commands, mut asset_commands: AssetCommands) { commands.spawn(( Name::new("Camera"), Camera3d::default(), @@ -56,8 +52,8 @@ fn setup( commands.spawn(( Name::new("Plane"), - Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))), - MeshMaterial3d(materials.add(StandardMaterial { + Mesh3d(asset_commands.spawn_asset(Mesh::from(Plane3d::default().mesh().size(5.0, 5.0)))), + MeshMaterial3d(asset_commands.spawn_asset(StandardMaterial { base_color: Color::srgb(0.3, 0.5, 0.3), // Turning off culling keeps the plane visible when viewed from beneath. cull_mode: None, @@ -67,8 +63,10 @@ fn setup( commands.spawn(( Name::new("Cube"), - Mesh3d(meshes.add(Cuboid::default())), - MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))), + Mesh3d(asset_commands.spawn_asset(Mesh::from(Cuboid::default()))), + MeshMaterial3d( + asset_commands.spawn_asset(StandardMaterial::from(Color::srgb(0.8, 0.7, 0.6))), + ), Transform::from_xyz(1.5, 0.51, 1.5), )); diff --git a/examples/camera/custom_projection.rs b/examples/camera/custom_projection.rs index 5a8f41fbbb107..54a6a69227776 100644 --- a/examples/camera/custom_projection.rs +++ b/examples/camera/custom_projection.rs @@ -47,11 +47,7 @@ impl CameraProjection for ObliquePerspectiveProjection { } } -fn setup( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { +fn setup(mut commands: Commands, mut asset_commands: AssetCommands) { commands.spawn(( Camera3d::default(), // Use our custom projection: @@ -65,13 +61,15 @@ fn setup( // Scene setup commands.spawn(( - Mesh3d(meshes.add(Circle::new(4.0))), - MeshMaterial3d(materials.add(Color::WHITE)), + Mesh3d(asset_commands.spawn_asset(Mesh::from(Circle::new(4.0)))), + MeshMaterial3d(asset_commands.spawn_asset(StandardMaterial::from(Color::WHITE))), Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), )); commands.spawn(( - Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), - MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))), + Mesh3d(asset_commands.spawn_asset(Mesh::from(Cuboid::new(1.0, 1.0, 1.0)))), + MeshMaterial3d( + asset_commands.spawn_asset(StandardMaterial::from(Color::srgb_u8(124, 144, 255))), + ), Transform::from_xyz(0.0, 0.5, 0.0), )); commands.spawn(( diff --git a/examples/camera/first_person_view_model.rs b/examples/camera/first_person_view_model.rs index eb6aeb7fcb5c4..2a96eaad8c3d0 100644 --- a/examples/camera/first_person_view_model.rs +++ b/examples/camera/first_person_view_model.rs @@ -96,13 +96,10 @@ const DEFAULT_RENDER_LAYER: usize = 0; /// The light source belongs to both layers. const VIEW_MODEL_RENDER_LAYER: usize = 1; -fn spawn_view_model( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { - let arm = meshes.add(Cuboid::new(0.1, 0.1, 0.5)); - let arm_material = materials.add(Color::from(tailwind::TEAL_200)); +fn spawn_view_model(mut commands: Commands, mut asset_commands: AssetCommands) { + let arm = asset_commands.spawn_asset(Mesh::from(Cuboid::new(0.1, 0.1, 0.5))); + let arm_material = + asset_commands.spawn_asset(StandardMaterial::from(Color::from(tailwind::TEAL_200))); commands.spawn(( Player, @@ -147,14 +144,10 @@ fn spawn_view_model( )); } -fn spawn_world_model( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { - let floor = meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(10.0))); - let cube = meshes.add(Cuboid::new(2.0, 0.5, 1.0)); - let material = materials.add(Color::WHITE); +fn spawn_world_model(mut commands: Commands, mut asset_commands: AssetCommands) { + let floor = asset_commands.spawn_asset(Mesh::from(Plane3d::new(Vec3::Y, Vec2::splat(10.0)))); + let cube = asset_commands.spawn_asset(Mesh::from(Cuboid::new(2.0, 0.5, 1.0))); + let material = asset_commands.spawn_asset(StandardMaterial::from(Color::WHITE)); // The world model camera will render the floor and the cubes spawned in this system. // Assigning no `RenderLayers` component defaults to layer 0. diff --git a/examples/camera/free_camera_controller.rs b/examples/camera/free_camera_controller.rs index 83859253839c5..0179fa625c210 100644 --- a/examples/camera/free_camera_controller.rs +++ b/examples/camera/free_camera_controller.rs @@ -236,19 +236,17 @@ fn spawn_lights(mut commands: Commands) { )); } -fn spawn_world( - mut commands: Commands, - mut materials: ResMut>, - mut meshes: ResMut>, -) { - let cube = meshes.add(Cuboid::new(1.0, 1.0, 1.0)); - let floor = meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(10.0))); - let sphere = meshes.add(Sphere::new(0.5)); - let wall = meshes.add(Cuboid::new(0.2, 4.0, 3.0)); +fn spawn_world(mut commands: Commands, mut asset_commands: AssetCommands) { + let cube = asset_commands.spawn_asset(Mesh::from(Cuboid::new(1.0, 1.0, 1.0))); + let floor = asset_commands.spawn_asset(Mesh::from(Plane3d::new(Vec3::Y, Vec2::splat(10.0)))); + let sphere = asset_commands.spawn_asset(Mesh::from(Sphere::new(0.5))); + let wall = asset_commands.spawn_asset(Mesh::from(Cuboid::new(0.2, 4.0, 3.0))); - let blue_material = materials.add(Color::from(tailwind::BLUE_700)); - let red_material = materials.add(Color::from(tailwind::RED_950)); - let white_material = materials.add(Color::WHITE); + let blue_material = + asset_commands.spawn_asset(StandardMaterial::from(Color::from(tailwind::BLUE_700))); + let red_material = + asset_commands.spawn_asset(StandardMaterial::from(Color::from(tailwind::RED_950))); + let white_material = asset_commands.spawn_asset(StandardMaterial::from(Color::WHITE)); // Top side of floor commands.spawn(( diff --git a/examples/camera/projection_zoom.rs b/examples/camera/projection_zoom.rs index 3797249f33ca5..5b571bbc1e825 100644 --- a/examples/camera/projection_zoom.rs +++ b/examples/camera/projection_zoom.rs @@ -48,8 +48,7 @@ fn setup( asset_server: Res, camera_settings: Res, mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, + mut asset_commands: AssetCommands, ) { commands.spawn(( Name::new("Camera"), @@ -70,8 +69,8 @@ fn setup( commands.spawn(( Name::new("Plane"), - Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))), - MeshMaterial3d(materials.add(StandardMaterial { + Mesh3d(asset_commands.spawn_asset(Mesh::from(Plane3d::default().mesh().size(5.0, 5.0)))), + MeshMaterial3d(asset_commands.spawn_asset(StandardMaterial { base_color: Color::srgb(0.3, 0.5, 0.3), // Turning off culling keeps the plane visible when viewed from beneath. cull_mode: None, diff --git a/examples/dev_tools/infinite_grid.rs b/examples/dev_tools/infinite_grid.rs index dd7796f983eff..677750c9649d7 100644 --- a/examples/dev_tools/infinite_grid.rs +++ b/examples/dev_tools/infinite_grid.rs @@ -22,11 +22,7 @@ fn main() { .run(); } -fn setup_system( - mut commands: Commands, - mut meshes: ResMut>, - mut standard_materials: ResMut>, -) { +fn setup_system(mut commands: Commands, mut asset_commands: AssetCommands) { commands.spawn(( // You need to spawn an entity with this component InfiniteGrid, @@ -47,9 +43,9 @@ fn setup_system( // cube commands.spawn(( - Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), + Mesh3d(asset_commands.spawn_asset(Mesh::from(Cuboid::new(1.0, 1.0, 1.0)))), MeshMaterial3d( - standard_materials.add(StandardMaterial::from_color(Color::srgba( + asset_commands.spawn_asset(StandardMaterial::from_color(Color::srgba( 1.0, 1.0, 1.0, 0.5, ))), ), @@ -57,9 +53,9 @@ fn setup_system( )); commands.spawn(( - Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), + Mesh3d(asset_commands.spawn_asset(Mesh::from(Cuboid::new(1.0, 1.0, 1.0)))), MeshMaterial3d( - standard_materials.add(StandardMaterial::from_color(Color::srgba( + asset_commands.spawn_asset(StandardMaterial::from_color(Color::srgba( 1.0, 1.0, 1.0, 0.5, ))), ), diff --git a/examples/diagnostics/log_diagnostics.rs b/examples/diagnostics/log_diagnostics.rs index f3049e7d92d3b..0de32b0547633 100644 --- a/examples/diagnostics/log_diagnostics.rs +++ b/examples/diagnostics/log_diagnostics.rs @@ -55,21 +55,19 @@ fn main() { } /// set up a simple 3D scene -fn setup( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { +fn setup(mut commands: Commands, mut asset_commands: AssetCommands) { // circular base commands.spawn(( - Mesh3d(meshes.add(Circle::new(4.0))), - MeshMaterial3d(materials.add(Color::WHITE)), + Mesh3d(asset_commands.spawn_asset(Mesh::from(Circle::new(4.0)))), + MeshMaterial3d(asset_commands.spawn_asset(StandardMaterial::from(Color::WHITE))), Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), )); // cube commands.spawn(( - Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), - MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))), + Mesh3d(asset_commands.spawn_asset(Mesh::from(Cuboid::new(1.0, 1.0, 1.0)))), + MeshMaterial3d( + asset_commands.spawn_asset(StandardMaterial::from(Color::srgb_u8(124, 144, 255))), + ), Transform::from_xyz(0.0, 0.5, 0.0), )); // light diff --git a/examples/ecs/entity_disabling.rs b/examples/ecs/entity_disabling.rs index 5360649f28ff4..4dba0bb9a02fa 100644 --- a/examples/ecs/entity_disabling.rs +++ b/examples/ecs/entity_disabling.rs @@ -101,20 +101,22 @@ fn reenable_entities_on_space( const X_EXTENT: f32 = 900.; -fn setup_scene( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { +fn setup_scene(mut commands: Commands, mut asset_commands: AssetCommands) { commands.spawn(Camera2d); let named_shapes = [ - (Name::new("Annulus"), meshes.add(Annulus::new(25.0, 50.0))), + ( + Name::new("Annulus"), + asset_commands.spawn_asset(Mesh::from(Annulus::new(25.0, 50.0))), + ), ( Name::new("Bestagon"), - meshes.add(RegularPolygon::new(50.0, 6)), + asset_commands.spawn_asset(Mesh::from(RegularPolygon::new(50.0, 6))), + ), + ( + Name::new("Rhombus"), + asset_commands.spawn_asset(Mesh::from(Rhombus::new(75.0, 100.0))), ), - (Name::new("Rhombus"), meshes.add(Rhombus::new(75.0, 100.0))), ]; let num_shapes = named_shapes.len(); @@ -126,7 +128,7 @@ fn setup_scene( name, DisableOnClick, Mesh2d(shape), - MeshMaterial2d(materials.add(color)), + MeshMaterial2d(asset_commands.spawn_asset(ColorMaterial::from(color))), Transform::from_xyz( // Distribute shapes from -X_EXTENT/2 to +X_EXTENT/2. -X_EXTENT / 2. + i as f32 / (num_shapes - 1) as f32 * X_EXTENT, diff --git a/examples/ecs/error_handling.rs b/examples/ecs/error_handling.rs index ca52b4f6dbad8..7e59e398f5a81 100644 --- a/examples/ecs/error_handling.rs +++ b/examples/ecs/error_handling.rs @@ -57,17 +57,15 @@ fn main() { /// An example of a system that calls several fallible functions with the question mark operator. /// /// See: -fn setup( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) -> Result { +fn setup(mut commands: Commands, mut asset_commands: AssetCommands) -> Result { let mut seeded_rng = ChaCha8Rng::seed_from_u64(19878367467712); // Make a plane for establishing space. commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(12.0, 12.0))), - MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), + Mesh3d(asset_commands.spawn_asset(Mesh::from(Plane3d::default().mesh().size(12.0, 12.0)))), + MeshMaterial3d( + asset_commands.spawn_asset(StandardMaterial::from(Color::srgb(0.3, 0.5, 0.3))), + ), Transform::from_xyz(0.0, -2.5, 0.0), )); @@ -92,8 +90,8 @@ fn setup( // Spawn the mesh into the scene: let mut sphere = commands.spawn(( - Mesh3d(meshes.add(sphere_mesh.clone())), - MeshMaterial3d(materials.add(StandardMaterial::default())), + Mesh3d(asset_commands.spawn_asset(sphere_mesh.clone())), + MeshMaterial3d(asset_commands.spawn_asset(StandardMaterial::default())), Transform::from_xyz(-1.0, 1.0, 0.0), )); @@ -102,8 +100,8 @@ fn setup( let distribution = UniformMeshSampler::try_new(triangles)?; // Setup sample points: - let point_mesh = meshes.add(Sphere::new(0.01).mesh().ico(3)?); - let point_material = materials.add(StandardMaterial { + let point_mesh = asset_commands.spawn_asset(Sphere::new(0.01).mesh().ico(3)?); + let point_material = asset_commands.spawn_asset(StandardMaterial { base_color: Srgba::RED.into(), emissive: LinearRgba::rgb(1.0, 0.0, 0.0), ..default() diff --git a/examples/ecs/iter_combinations.rs b/examples/ecs/iter_combinations.rs index 0e998ed508b7f..60c76cfa30a82 100644 --- a/examples/ecs/iter_combinations.rs +++ b/examples/ecs/iter_combinations.rs @@ -38,10 +38,9 @@ struct BodyBundle { fn generate_bodies( time: Res>, mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, + mut asset_commands: AssetCommands, ) { - let mesh = meshes.add(Sphere::new(1.0).mesh().ico(3).unwrap()); + let mesh = asset_commands.spawn_asset(Sphere::new(1.0).mesh().ico(3).unwrap()); let color_range = 0.5..1.0; let vel_range = -0.5..0.5; @@ -65,10 +64,12 @@ fn generate_bodies( commands.spawn(( BodyBundle { mesh: Mesh3d(mesh.clone()), - material: MeshMaterial3d(materials.add(Color::srgb( - rng.random_range(color_range.clone()), - rng.random_range(color_range.clone()), - rng.random_range(color_range.clone()), + material: MeshMaterial3d(asset_commands.spawn_asset(StandardMaterial::from( + Color::srgb( + rng.random_range(color_range.clone()), + rng.random_range(color_range.clone()), + rng.random_range(color_range.clone()), + ), ))), mass: Mass(mass_value), acceleration: Acceleration(Vec3::ZERO), @@ -94,8 +95,8 @@ fn generate_bodies( commands .spawn(( BodyBundle { - mesh: Mesh3d(meshes.add(Sphere::new(1.0).mesh().ico(5).unwrap())), - material: MeshMaterial3d(materials.add(StandardMaterial { + mesh: Mesh3d(asset_commands.spawn_asset(Sphere::new(1.0).mesh().ico(5).unwrap())), + material: MeshMaterial3d(asset_commands.spawn_asset(StandardMaterial { base_color: ORANGE_RED.into(), emissive: LinearRgba::from(ORANGE_RED) * 2., ..default() diff --git a/examples/gizmos/3d_gizmos.rs b/examples/gizmos/3d_gizmos.rs index 9da5f5a30a72a..6c629e96eaaf7 100644 --- a/examples/gizmos/3d_gizmos.rs +++ b/examples/gizmos/3d_gizmos.rs @@ -20,12 +20,7 @@ fn main() { #[derive(Default, Reflect, GizmoConfigGroup)] struct MyRoundGizmos; -fn setup( - mut commands: Commands, - mut gizmo_assets: ResMut>, - mut meshes: ResMut>, - mut materials: ResMut>, -) { +fn setup(mut commands: Commands, mut asset_commands: AssetCommands) { let mut gizmo = GizmoAsset::new(); // When drawing a lot of static lines a Gizmo component can have @@ -39,7 +34,7 @@ fn setup( commands.spawn(( Gizmo { - handle: gizmo_assets.add(gizmo), + handle: asset_commands.spawn_asset(gizmo), line_config: GizmoLineConfig { width: 5., ..default() @@ -56,13 +51,17 @@ fn setup( )); // plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))), - MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), + Mesh3d(asset_commands.spawn_asset(Mesh::from(Plane3d::default().mesh().size(5.0, 5.0)))), + MeshMaterial3d( + asset_commands.spawn_asset(StandardMaterial::from(Color::srgb(0.3, 0.5, 0.3))), + ), )); // cube commands.spawn(( - Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), - MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))), + Mesh3d(asset_commands.spawn_asset(Mesh::from(Cuboid::new(1.0, 1.0, 1.0)))), + MeshMaterial3d( + asset_commands.spawn_asset(StandardMaterial::from(Color::srgb(0.8, 0.7, 0.6))), + ), Transform::from_xyz(0.0, 0.5, 0.0), )); // light diff --git a/examples/gizmos/axes.rs b/examples/gizmos/axes.rs index 71fd1ece46882..7e74e8b8c4f2e 100644 --- a/examples/gizmos/axes.rs +++ b/examples/gizmos/axes.rs @@ -37,11 +37,7 @@ struct TransformTracking { #[derive(Resource)] struct SeededRng(ChaCha8Rng); -fn setup( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { +fn setup(mut commands: Commands, mut asset_commands: AssetCommands) { // We're seeding the PRNG here to make this example deterministic for testing purposes. // This isn't strictly required in practical use unless you need your app to be deterministic. let mut rng = ChaCha8Rng::seed_from_u64(19878367467713); @@ -63,8 +59,10 @@ fn setup( // Action! (Our cubes that are going to move) commands.spawn(( - Mesh3d(meshes.add(Cuboid::new(1., 1., 1.))), - MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))), + Mesh3d(asset_commands.spawn_asset(Mesh::from(Cuboid::new(1., 1., 1.)))), + MeshMaterial3d( + asset_commands.spawn_asset(StandardMaterial::from(Color::srgb(0.8, 0.7, 0.6))), + ), ShowAxes, TransformTracking { initial_transform: default(), @@ -74,8 +72,10 @@ fn setup( )); commands.spawn(( - Mesh3d(meshes.add(Cuboid::new(0.5, 0.5, 0.5))), - MeshMaterial3d(materials.add(Color::srgb(0.6, 0.7, 0.8))), + Mesh3d(asset_commands.spawn_asset(Mesh::from(Cuboid::new(0.5, 0.5, 0.5)))), + MeshMaterial3d( + asset_commands.spawn_asset(StandardMaterial::from(Color::srgb(0.6, 0.7, 0.8))), + ), ShowAxes, TransformTracking { initial_transform: default(), @@ -86,8 +86,10 @@ fn setup( // A plane to give a sense of place commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(20., 20.))), - MeshMaterial3d(materials.add(Color::srgb(0.1, 0.1, 0.1))), + Mesh3d(asset_commands.spawn_asset(Mesh::from(Plane3d::default().mesh().size(20., 20.)))), + MeshMaterial3d( + asset_commands.spawn_asset(StandardMaterial::from(Color::srgb(0.1, 0.1, 0.1))), + ), Transform::from_xyz(0., -2., 0.), )); diff --git a/examples/gizmos/light_gizmos.rs b/examples/gizmos/light_gizmos.rs index e8356411e2d9e..0a5edfc21c882 100644 --- a/examples/gizmos/light_gizmos.rs +++ b/examples/gizmos/light_gizmos.rs @@ -38,21 +38,21 @@ fn gizmo_color_text(config: &LightGizmoConfigGroup) -> String { fn setup( mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, + mut asset_commands: AssetCommands, mut config_store: ResMut, ) { // Circular base. commands.spawn(( - Mesh3d(meshes.add(Circle::new(4.0))), - MeshMaterial3d(materials.add(Color::WHITE)), + Mesh3d(asset_commands.spawn_asset(Mesh::from(Circle::new(4.0)))), + MeshMaterial3d(asset_commands.spawn_asset(StandardMaterial::from(Color::WHITE))), Transform::from_rotation(Quat::from_rotation_x(-FRAC_PI_2)), )); // Cubes. { - let mesh = meshes.add(Cuboid::new(1.0, 1.0, 1.0)); - let material = materials.add(Color::srgb_u8(124, 144, 255)); + let mesh = asset_commands.spawn_asset(Mesh::from(Cuboid::new(1.0, 1.0, 1.0))); + let material = + asset_commands.spawn_asset(StandardMaterial::from(Color::srgb_u8(124, 144, 255))); for x in [-2.0, 0.0, 2.0] { commands.spawn(( Mesh3d(mesh.clone()), diff --git a/examples/gizmos/transform_gizmo.rs b/examples/gizmos/transform_gizmo.rs index ee21a21d1152d..1aa1da40e1209 100644 --- a/examples/gizmos/transform_gizmo.rs +++ b/examples/gizmos/transform_gizmo.rs @@ -28,11 +28,7 @@ fn main() { .run(); } -fn setup( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { +fn setup(mut commands: Commands, mut asset_commands: AssetCommands) { // Instructions commands.spawn(( Text::new( @@ -49,8 +45,10 @@ fn setup( // Ground plane (not pickable) commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(10.0, 10.0))), - MeshMaterial3d(materials.add(Color::srgb(0.3, 0.3, 0.3))), + Mesh3d(asset_commands.spawn_asset(Mesh::from(Plane3d::default().mesh().size(10.0, 10.0)))), + MeshMaterial3d( + asset_commands.spawn_asset(StandardMaterial::from(Color::srgb(0.3, 0.3, 0.3))), + ), Pickable::IGNORE, )); @@ -58,8 +56,10 @@ fn setup( // The parent cube is selected by default. commands .spawn(( - Mesh3d(meshes.add(Cuboid::new(1.5, 0.15, 1.0))), - MeshMaterial3d(materials.add(Color::srgb(0.8, 0.3, 0.3))), + Mesh3d(asset_commands.spawn_asset(Mesh::from(Cuboid::new(1.5, 0.15, 1.0)))), + MeshMaterial3d( + asset_commands.spawn_asset(StandardMaterial::from(Color::srgb(0.8, 0.3, 0.3))), + ), Transform::from_xyz(-2.0, 1.0, 0.0), TransformGizmoFocus, )) @@ -67,26 +67,34 @@ fn setup( .with_children(|parent| { // Table leg (child) parent.spawn(( - Mesh3d(meshes.add(Cuboid::new(0.1, 0.85, 0.1))), - MeshMaterial3d(materials.add(Color::srgb(0.6, 0.2, 0.2))), + Mesh3d(asset_commands.spawn_asset(Mesh::from(Cuboid::new(0.1, 0.85, 0.1)))), + MeshMaterial3d( + asset_commands.spawn_asset(StandardMaterial::from(Color::srgb(0.6, 0.2, 0.2))), + ), Transform::from_xyz(-0.6, -0.5, 0.4), Pickable::IGNORE, )); parent.spawn(( - Mesh3d(meshes.add(Cuboid::new(0.1, 0.85, 0.1))), - MeshMaterial3d(materials.add(Color::srgb(0.6, 0.2, 0.2))), + Mesh3d(asset_commands.spawn_asset(Mesh::from(Cuboid::new(0.1, 0.85, 0.1)))), + MeshMaterial3d( + asset_commands.spawn_asset(StandardMaterial::from(Color::srgb(0.6, 0.2, 0.2))), + ), Transform::from_xyz(0.6, -0.5, 0.4), Pickable::IGNORE, )); parent.spawn(( - Mesh3d(meshes.add(Cuboid::new(0.1, 0.85, 0.1))), - MeshMaterial3d(materials.add(Color::srgb(0.6, 0.2, 0.2))), + Mesh3d(asset_commands.spawn_asset(Mesh::from(Cuboid::new(0.1, 0.85, 0.1)))), + MeshMaterial3d( + asset_commands.spawn_asset(StandardMaterial::from(Color::srgb(0.6, 0.2, 0.2))), + ), Transform::from_xyz(-0.6, -0.5, -0.4), Pickable::IGNORE, )); parent.spawn(( - Mesh3d(meshes.add(Cuboid::new(0.1, 0.85, 0.1))), - MeshMaterial3d(materials.add(Color::srgb(0.6, 0.2, 0.2))), + Mesh3d(asset_commands.spawn_asset(Mesh::from(Cuboid::new(0.1, 0.85, 0.1)))), + MeshMaterial3d( + asset_commands.spawn_asset(StandardMaterial::from(Color::srgb(0.6, 0.2, 0.2))), + ), Transform::from_xyz(0.6, -0.5, -0.4), Pickable::IGNORE, )); @@ -95,8 +103,10 @@ fn setup( // Standalone cube commands .spawn(( - Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), - MeshMaterial3d(materials.add(Color::srgb(0.3, 0.8, 0.3))), + Mesh3d(asset_commands.spawn_asset(Mesh::from(Cuboid::new(1.0, 1.0, 1.0)))), + MeshMaterial3d( + asset_commands.spawn_asset(StandardMaterial::from(Color::srgb(0.3, 0.8, 0.3))), + ), Transform::from_xyz(2.0, 0.5, 0.0), )) .observe(on_click_select); diff --git a/examples/gltf/custom_gltf_vertex_attribute.rs b/examples/gltf/custom_gltf_vertex_attribute.rs index f523ae2cdf71b..a8ecb004a84bb 100644 --- a/examples/gltf/custom_gltf_vertex_attribute.rs +++ b/examples/gltf/custom_gltf_vertex_attribute.rs @@ -45,8 +45,8 @@ fn main() { fn setup( mut commands: Commands, + mut asset_commands: AssetCommands, asset_server: Res, - mut materials: ResMut>, ) { // Add a mesh loaded from a glTF file. This mesh has data for `ATTRIBUTE_BARYCENTRIC`. let mesh = asset_server.load( @@ -58,7 +58,7 @@ fn setup( ); commands.spawn(( Mesh2d(mesh), - MeshMaterial2d(materials.add(CustomMaterial {})), + MeshMaterial2d(asset_commands.spawn_asset(CustomMaterial {})), Transform::from_scale(150.0 * Vec3::ONE), )); diff --git a/examples/gltf/edit_material_on_gltf.rs b/examples/gltf/edit_material_on_gltf.rs index 894ca22408686..ca736daa7dce3 100644 --- a/examples/gltf/edit_material_on_gltf.rs +++ b/examples/gltf/edit_material_on_gltf.rs @@ -55,10 +55,11 @@ fn setup_scene(mut commands: Commands, asset_server: Res) { fn change_material( scene_ready: On, mut commands: Commands, + mut asset_commands: AssetCommands, children: Query<&Children>, color_override: Query<&ColorOverride>, mesh_materials: Query<(&MeshMaterial3d, &GltfMaterialName)>, - mut asset_materials: ResMut>, + asset_materials: Assets, ) { info!("processing Scene Entity: {}", scene_ready.entity); @@ -93,7 +94,7 @@ fn change_material( // Override `MeshMaterial3d` with new material commands .entity(descendant) - .insert(MeshMaterial3d(asset_materials.add(new_material))); + .insert(MeshMaterial3d(asset_commands.spawn_asset(new_material))); } name => { info!("not replacing: {name}"); diff --git a/examples/gltf/gltf_extension_animation_graph.rs b/examples/gltf/gltf_extension_animation_graph.rs index c5d89ff646d90..8b53e8e7c2f63 100644 --- a/examples/gltf/gltf_extension_animation_graph.rs +++ b/examples/gltf/gltf_extension_animation_graph.rs @@ -86,11 +86,7 @@ fn play_animation_when_ready( } /// Spawn a camera and a simple environment with a ground plane and light. -fn setup_camera_and_environment( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { +fn setup_camera_and_environment(mut commands: Commands, mut asset_commands: AssetCommands) { // Camera commands.spawn(( Camera3d::default(), @@ -99,8 +95,12 @@ fn setup_camera_and_environment( // Plane commands.spawn(( - Mesh3d(meshes.add(Plane3d::default().mesh().size(500000.0, 500000.0))), - MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), + Mesh3d(asset_commands.spawn_asset(Mesh::from( + Plane3d::default().mesh().size(500000.0, 500000.0), + ))), + MeshMaterial3d( + asset_commands.spawn_asset(StandardMaterial::from(Color::srgb(0.3, 0.5, 0.3))), + ), )); // Light @@ -315,8 +315,8 @@ struct ParticleAssets { impl FromWorld for ParticleAssets { fn from_world(world: &mut World) -> Self { Self { - mesh: world.add_asset::(Sphere::new(10.0)), - material: world.add_asset::(StandardMaterial { + mesh: world.spawn_asset(Mesh::from(Sphere::new(10.0))), + material: world.spawn_asset(StandardMaterial { base_color: Color::WHITE, ..Default::default() }), diff --git a/examples/gltf/query_gltf_primitives.rs b/examples/gltf/query_gltf_primitives.rs index 3f1a4e57b8dbe..bb5f55ffec7b7 100644 --- a/examples/gltf/query_gltf_primitives.rs +++ b/examples/gltf/query_gltf_primitives.rs @@ -14,8 +14,8 @@ fn main() { } fn find_top_material_and_mesh( - mut materials: ResMut>, - mut meshes: ResMut>, + mut materials: AssetsMut, + mut meshes: AssetsMut, time: Res