diff --git a/benches/benches/bevy_scene/spawn.rs b/benches/benches/bevy_scene/spawn.rs index 1f1599f9ccc38..1e39b13a78f1b 100644 --- a/benches/benches/bevy_scene/spawn.rs +++ b/benches/benches/bevy_scene/spawn.rs @@ -5,11 +5,12 @@ use std::{path::Path, time::Duration}; use bevy_app::App; use bevy_asset::{ + asset_value, io::{ memory::{Dir, MemoryAssetReader}, AssetSourceBuilder, AssetSourceId, }, - AssetApp, AssetLoader, AssetServer, Assets, + Asset, AssetApp, AssetLoader, AssetServer, Assets, Handle, }; use bevy_ecs::prelude::*; use bevy_scene::{prelude::*, ScenePatch}; @@ -17,6 +18,146 @@ use bevy_ui::prelude::*; criterion_group!(benches, spawn); +fn spawn(c: &mut Criterion) { + let mut group = c.benchmark_group("spawn"); + group.warm_up_time(Duration::from_millis(500)); + group.measurement_time(Duration::from_secs(4)); + group.bench_function("ui_immediate_function_scene", |b| { + let mut app = bench_app(|_| {}, |_| {}); + b.iter(move || { + app.world_mut().spawn_scene(ui()).unwrap(); + }); + }); + group.bench_function("ui_immediate_loaded_scene", |b| { + let dir = Dir::default(); + let mut app = bench_app( + |app| { + in_memory_asset_source(dir.clone(), app); + }, + |app| { + app.register_asset_loader(FakeSceneLoader::new(button)); + }, + ); + + // Insert an asset that the fake loader can fake read. + dir.insert_asset_text(Path::new("button.bsn"), ""); + + let asset_server = app.world().resource::().clone(); + let handle = asset_server.load("button.bsn"); + + run_app_until(&mut app, || asset_server.is_loaded(&handle)); + + let patch = app + .world() + .resource::>() + .get(&handle) + .unwrap(); + assert!(patch.resolved.is_some()); + + b.iter(move || { + app.world_mut().spawn_scene(ui_loaded_asset()).unwrap(); + }); + + drop(handle); + }); + group.bench_function("ui_raw_bundle_no_scene", |b| { + let mut app = bench_app(|_| {}, |_| {}); + + b.iter(move || { + app.world_mut().spawn(raw_ui()); + }); + }); + + group.bench_function("handle_template_handle", |b| { + let dir = Dir::default(); + let mut app = bench_app( + |app| { + in_memory_asset_source(dir.clone(), app); + }, + |app| { + app.init_asset::(); + let assets = app.world().resource::(); + let handles = (0..10).map(|_| assets.add(EmptyAsset)).collect::>(); + app.register_asset_loader(FakeSceneLoader::new(move || { + asset_handle_scene(handles.clone()) + })); + }, + ); + + dir.insert_asset_text(Path::new("a.bsn"), ""); + + let asset_server = app.world().resource::().clone(); + let handle = asset_server.load::("a.bsn"); + + run_app_until(&mut app, || asset_server.is_loaded(&handle)); + + let world = app.world_mut(); + b.iter(|| { + for _ in 0..100 { + world.spawn_scene(bsn! { :"a.bsn" }).unwrap(); + } + }); + }); + + group.bench_function("handle_template_value", |b| { + let dir = Dir::default(); + let mut app = bench_app( + |app| { + in_memory_asset_source(dir.clone(), app); + }, + |app| { + app.register_asset_loader(FakeSceneLoader::new(asset_value_scene)); + app.init_asset::(); + }, + ); + + dir.insert_asset_text(Path::new("a.bsn"), ""); + + let asset_server = app.world().resource::().clone(); + let handle = asset_server.load::("a.bsn"); + + run_app_until(&mut app, || asset_server.is_loaded(&handle)); + + let world = app.world_mut(); + b.iter(|| { + for _ in 0..100 { + world.spawn_scene(bsn! { :"a.bsn" }).unwrap(); + } + }); + }); + group.finish(); +} + +#[derive(Asset, TypePath)] +struct EmptyAsset; + +#[derive(Component, FromTemplate)] +#[expect(unused, reason = "this is just used for init")] +struct AssetReference(Handle); + +fn asset_value_scene() -> impl Scene { + let children = (0..10) + .map(|_| { + bsn! {AssetReference(asset_value(EmptyAsset))} + }) + .collect::>(); + bsn! { + Children [{children}] + } +} + +fn asset_handle_scene(mut handles: Vec>) -> impl Scene { + let children = handles + .drain(..) + .map(|handle| { + bsn! {AssetReference({handle.clone()})} + }) + .collect::>(); + bsn! { + Children [{children}] + } +} + fn ui() -> impl Scene { bsn! { Node @@ -209,88 +350,47 @@ fn run_app_until(app: &mut App, mut predicate: impl FnMut() -> bool) { panic!("Ran out of loops to return `Some` from `predicate`"); } -fn spawn(c: &mut Criterion) { - let mut group = c.benchmark_group("spawn"); - group.warm_up_time(Duration::from_millis(500)); - group.measurement_time(Duration::from_secs(4)); - group.bench_function("ui_immediate_function_scene", |b| { - let mut app = App::new(); - app.add_plugins((bevy_asset::AssetPlugin::default(), bevy_scene::ScenePlugin)); - - b.iter(move || { - app.world_mut().spawn_scene(ui()).unwrap(); - }); - }); - group.bench_function("ui_immediate_loaded_scene", |b| { - let mut app = App::new(); - let dir = Dir::default(); - let dir_clone = dir.clone(); - app.register_asset_source( - AssetSourceId::Default, - AssetSourceBuilder::new(move || { - Box::new(MemoryAssetReader { - root: dir_clone.clone(), - }) - }), - ); - app.add_plugins(( - bevy_app::TaskPoolPlugin::default(), - bevy_asset::AssetPlugin::default(), - bevy_scene::ScenePlugin, - )); - app.finish(); - app.cleanup(); - - // Create a fake loader to act as a ScenePatch loaded from a file. - app.register_asset_loader(FakeSceneLoader); - - #[derive(TypePath)] - struct FakeSceneLoader; - - impl AssetLoader for FakeSceneLoader { - type Asset = ScenePatch; - type Error = std::io::Error; - type Settings = (); - - async fn load( - &self, - _reader: &mut dyn bevy_asset::io::Reader, - _settings: &Self::Settings, - load_context: &mut bevy_asset::LoadContext<'_>, - ) -> Result { - Ok(ScenePatch::load_with(load_context, button())) - } - } - - // Insert an asset that the fake loader can fake read. - dir.insert_asset_text(Path::new("button.bsn"), ""); - - let asset_server = app.world().resource::().clone(); - let handle = asset_server.load("button.bsn"); - assert!(app.world().get_resource::>().is_some()); +fn bench_app(before: impl FnOnce(&mut App), after: impl FnOnce(&mut App)) -> App { + let mut app = App::new(); + before(&mut app); + app.add_plugins(( + bevy_app::TaskPoolPlugin::default(), + bevy_asset::AssetPlugin::default(), + bevy_scene::ScenePlugin, + )); + after(&mut app); + app.finish(); + app.cleanup(); + app +} - run_app_until(&mut app, || asset_server.is_loaded(&handle)); +fn in_memory_asset_source(dir: Dir, app: &mut App) { + app.register_asset_source( + AssetSourceId::Default, + AssetSourceBuilder::new(move || Box::new(MemoryAssetReader { root: dir.clone() })), + ); +} - let patch = app - .world() - .resource::>() - .get(&handle) - .unwrap(); - assert!(patch.resolved.is_some()); +#[derive(TypePath)] +struct FakeSceneLoader(Box Box + Send + Sync>); - b.iter(move || { - app.world_mut().spawn_scene(ui_loaded_asset()).unwrap(); - }); +impl FakeSceneLoader { + pub fn new(scene_fn: impl (Fn() -> S) + Send + Sync + 'static) -> Self { + Self(Box::new(move || Box::new(scene_fn()))) + } +} - drop(handle); - }); - group.bench_function("ui_raw_bundle_no_scene", |b| { - let mut app = App::new(); - app.add_plugins((bevy_asset::AssetPlugin::default(), bevy_scene::ScenePlugin)); +impl AssetLoader for FakeSceneLoader { + type Asset = ScenePatch; + type Error = std::io::Error; + type Settings = (); - b.iter(move || { - app.world_mut().spawn(raw_ui()); - }); - }); - group.finish(); + async fn load( + &self, + _reader: &mut dyn bevy_asset::io::Reader, + _settings: &Self::Settings, + load_context: &mut bevy_asset::LoadContext<'_>, + ) -> Result { + Ok(ScenePatch::load_with(load_context, (self.0)())) + } } diff --git a/crates/bevy_asset/src/handle.rs b/crates/bevy_asset/src/handle.rs index 3b28389982aee..1dcecc8ff5d4a 100644 --- a/crates/bevy_asset/src/handle.rs +++ b/crates/bevy_asset/src/handle.rs @@ -4,7 +4,7 @@ use crate::{ }; use alloc::sync::Arc; use bevy_ecs::template::{FromTemplate, SpecializeFromTemplate, Template, TemplateContext}; -use bevy_platform::collections::Equivalent; +use bevy_platform::{collections::Equivalent, sync::Mutex}; use bevy_reflect::{Reflect, TypePath}; use core::{ any::TypeId, @@ -208,10 +208,56 @@ impl FromTemplate for Handle { type Template = HandleTemplate; } +/// A [`Template`] that produces a [`Handle`]. #[derive(Reflect)] pub enum HandleTemplate { + /// Creates a [`Handle`] by calling [`AssetServer::load`] on the given [`AssetPath`]. Path(AssetPath<'static>), + /// Creates a [`Handle`] by cloning the given [`Handle`] value. Handle(Handle), + /// Creates a [`Handle`] by adding the given asset value using [`AssetServer::add`]. This will + /// cache the resulting [`Handle`] on the template and reuse it for future template builds. + /// + /// This should generally be constructed using [`HandleTemplate::value`] or [`asset_value`]. + Value(ArcMutexValue), +} + +impl HandleTemplate { + /// This will create a new [`HandleTemplate`] for the given `asset` value. This makes it possible + /// to define assets "inline" in templates / scenes that produce a [`Handle`]. + /// + /// This supports [`Into`] + /// to automatically convert values that can become `A`. + pub fn value(value: impl Into) -> Self { + HandleTemplate::Value(ArcMutexValue(Arc::new(Mutex::new(AssetOrHandle::Value( + Some(value.into()), + ))))) + } +} + +/// Stores an [`Arc>>`]. +/// +/// This intermediary type exists largely to enable reflect(opaque). +#[derive(Reflect)] +#[reflect(opaque)] +pub struct ArcMutexValue(Arc>>); + +impl Clone for ArcMutexValue { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +#[derive(Reflect)] +enum AssetOrHandle { + Value(Option), + Handle(Handle), +} + +impl Default for AssetOrHandle { + fn default() -> Self { + Self::Handle(Default::default()) + } } impl Default for HandleTemplate { @@ -238,6 +284,21 @@ impl Template for HandleTemplate { Ok(match self { HandleTemplate::Path(asset_path) => context.resource::().load(asset_path), HandleTemplate::Handle(handle) => handle.clone(), + HandleTemplate::Value(value) => { + // This unwrap is ok. If another caller panicked while holding this mutex, then the + // program is in an invalid state and this should panic too. + let mut value_or_handle = value.0.lock().unwrap(); + match &mut *value_or_handle { + AssetOrHandle::Value(value) => { + // This unwrap is ok because AssetOrHandle::Value will always either contain a Some Value + // when it is in this state (AssetOrHandle is private). + let handle = context.resource::().add(value.take().unwrap()); + *value_or_handle = AssetOrHandle::Handle(handle.clone()); + handle + } + AssetOrHandle::Handle(handle) => handle.clone(), + } + } }) } @@ -245,9 +306,20 @@ impl Template for HandleTemplate { match self { HandleTemplate::Path(asset_path) => HandleTemplate::Path(asset_path.clone()), HandleTemplate::Handle(handle) => HandleTemplate::Handle(handle.clone()), + HandleTemplate::Value(value) => HandleTemplate::Value(value.clone()), } } } + +/// This will create a new [`HandleTemplate`] for the given `asset` value. This makes it possible +/// to define assets "inline" in templates / scenes that produce a [`Handle`]. +/// +/// This supports [`Into`] +/// to automatically convert values that can become `A`. +pub fn asset_value, A: Asset>(asset: I) -> HandleTemplate { + HandleTemplate::value(asset) +} + impl core::fmt::Debug for Handle { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { let name = ShortName::of::(); diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 70fb5b25b68af..c97cc806a01ab 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -165,8 +165,8 @@ pub mod prelude { #[doc(hidden)] pub use crate::{ - Asset, AssetApp, AssetEvent, AssetId, AssetMode, AssetPlugin, AssetServer, Assets, - DirectAssetAccessExt, Handle, UntypedHandle, + asset_value, Asset, AssetApp, AssetEvent, AssetId, AssetMode, AssetPlugin, AssetServer, + Assets, DirectAssetAccessExt, Handle, UntypedHandle, }; } diff --git a/crates/bevy_reflect/src/impls/bevy_platform/sync.rs b/crates/bevy_reflect/src/impls/bevy_platform/sync.rs index ba3234177f999..dbf61f4cf6318 100644 --- a/crates/bevy_reflect/src/impls/bevy_platform/sync.rs +++ b/crates/bevy_reflect/src/impls/bevy_platform/sync.rs @@ -1,3 +1,4 @@ -use bevy_reflect_derive::impl_reflect_opaque; +use bevy_reflect_derive::{impl_reflect_opaque, impl_type_path}; impl_reflect_opaque!(::bevy_platform::sync::Arc(Clone)); +impl_type_path!(::bevy_platform::sync::Mutex); diff --git a/examples/3d/3d_scene.rs b/examples/3d/3d_scene.rs index b98ca52ae9a5b..d3584be4c4f5e 100644 --- a/examples/3d/3d_scene.rs +++ b/examples/3d/3d_scene.rs @@ -10,34 +10,30 @@ fn main() { } /// set up a simple 3D scene -fn setup( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { - // circular base - commands.spawn(( - Mesh3d(meshes.add(Circle::new(4.0))), - MeshMaterial3d(materials.add(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))), - Transform::from_xyz(0.0, 0.5, 0.0), - )); - // light - commands.spawn(( - PointLight { - shadow_maps_enabled: true, - ..default() - }, - Transform::from_xyz(4.0, 8.0, 4.0), - )); - // camera - commands.spawn(( - Camera3d::default(), - Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), - )); +fn setup(world: &mut World) -> Result { + world.spawn_scene_list(bsn_list! [ + ( + #CircularBase + Mesh3d(asset_value(Circle::new(4.0))) + MeshMaterial3d::(asset_value(Color::WHITE)) + Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)) + ), + ( + #Cube + Mesh3d(asset_value(Cuboid::new(1.0, 1.0, 1.0))) + MeshMaterial3d::(asset_value(Color::srgb_u8(124, 144, 255))) + Transform::from_xyz(0.0, 0.5, 0.0) + ), + ( + PointLight { + shadow_maps_enabled: true, + } + Transform::from_xyz(4.0, 8.0, 4.0) + ), + ( + Camera3d + template_value(Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y)) + ) + ])?; + Ok(()) }