Skip to content

Commit 6e9522c

Browse files
authored
Asset Value Templates via asset_value() and HandleTemplate::Value (#23839)
# Objective Temporary simple / suboptimal solution to #23822 It would be very nice to be able to define assets inline in BSN. ## Solution - Add a new `HandleTemplate::Value` variant, which contains an `Arc<Mutex<AssetOrHandle<T>>>` - This template, when first applied, will lock the mutex, insert the asset value as a new asset, cache the handle in the `Arc<Mutex<HandleOrTemplate>` and return the handle. Future calls will lock the mutex and return the cached handle. - Add `asset_value(SOME_ASSET)` function to improve ergonomics. - Port `3d_scene` to illustrate Doing a lock on every spawn is obviously suboptimal. The long term plan for "inline assets in BSN" is defined in #23822 and will not use this locking approach.
1 parent 8b8a432 commit 6e9522c

File tree

5 files changed

+283
-114
lines changed

5 files changed

+283
-114
lines changed

benches/benches/bevy_scene/spawn.rs

Lines changed: 180 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,159 @@ use std::{path::Path, time::Duration};
55

66
use bevy_app::App;
77
use bevy_asset::{
8+
asset_value,
89
io::{
910
memory::{Dir, MemoryAssetReader},
1011
AssetSourceBuilder, AssetSourceId,
1112
},
12-
AssetApp, AssetLoader, AssetServer, Assets,
13+
Asset, AssetApp, AssetLoader, AssetServer, Assets, Handle,
1314
};
1415
use bevy_ecs::prelude::*;
1516
use bevy_scene::{prelude::*, ScenePatch};
1617
use bevy_ui::prelude::*;
1718

1819
criterion_group!(benches, spawn);
1920

21+
fn spawn(c: &mut Criterion) {
22+
let mut group = c.benchmark_group("spawn");
23+
group.warm_up_time(Duration::from_millis(500));
24+
group.measurement_time(Duration::from_secs(4));
25+
group.bench_function("ui_immediate_function_scene", |b| {
26+
let mut app = bench_app(|_| {}, |_| {});
27+
b.iter(move || {
28+
app.world_mut().spawn_scene(ui()).unwrap();
29+
});
30+
});
31+
group.bench_function("ui_immediate_loaded_scene", |b| {
32+
let dir = Dir::default();
33+
let mut app = bench_app(
34+
|app| {
35+
in_memory_asset_source(dir.clone(), app);
36+
},
37+
|app| {
38+
app.register_asset_loader(FakeSceneLoader::new(button));
39+
},
40+
);
41+
42+
// Insert an asset that the fake loader can fake read.
43+
dir.insert_asset_text(Path::new("button.bsn"), "");
44+
45+
let asset_server = app.world().resource::<AssetServer>().clone();
46+
let handle = asset_server.load("button.bsn");
47+
48+
run_app_until(&mut app, || asset_server.is_loaded(&handle));
49+
50+
let patch = app
51+
.world()
52+
.resource::<Assets<ScenePatch>>()
53+
.get(&handle)
54+
.unwrap();
55+
assert!(patch.resolved.is_some());
56+
57+
b.iter(move || {
58+
app.world_mut().spawn_scene(ui_loaded_asset()).unwrap();
59+
});
60+
61+
drop(handle);
62+
});
63+
group.bench_function("ui_raw_bundle_no_scene", |b| {
64+
let mut app = bench_app(|_| {}, |_| {});
65+
66+
b.iter(move || {
67+
app.world_mut().spawn(raw_ui());
68+
});
69+
});
70+
71+
group.bench_function("handle_template_handle", |b| {
72+
let dir = Dir::default();
73+
let mut app = bench_app(
74+
|app| {
75+
in_memory_asset_source(dir.clone(), app);
76+
},
77+
|app| {
78+
app.init_asset::<EmptyAsset>();
79+
let assets = app.world().resource::<AssetServer>();
80+
let handles = (0..10).map(|_| assets.add(EmptyAsset)).collect::<Vec<_>>();
81+
app.register_asset_loader(FakeSceneLoader::new(move || {
82+
asset_handle_scene(handles.clone())
83+
}));
84+
},
85+
);
86+
87+
dir.insert_asset_text(Path::new("a.bsn"), "");
88+
89+
let asset_server = app.world().resource::<AssetServer>().clone();
90+
let handle = asset_server.load::<ScenePatch>("a.bsn");
91+
92+
run_app_until(&mut app, || asset_server.is_loaded(&handle));
93+
94+
let world = app.world_mut();
95+
b.iter(|| {
96+
for _ in 0..100 {
97+
world.spawn_scene(bsn! { :"a.bsn" }).unwrap();
98+
}
99+
});
100+
});
101+
102+
group.bench_function("handle_template_value", |b| {
103+
let dir = Dir::default();
104+
let mut app = bench_app(
105+
|app| {
106+
in_memory_asset_source(dir.clone(), app);
107+
},
108+
|app| {
109+
app.register_asset_loader(FakeSceneLoader::new(asset_value_scene));
110+
app.init_asset::<EmptyAsset>();
111+
},
112+
);
113+
114+
dir.insert_asset_text(Path::new("a.bsn"), "");
115+
116+
let asset_server = app.world().resource::<AssetServer>().clone();
117+
let handle = asset_server.load::<ScenePatch>("a.bsn");
118+
119+
run_app_until(&mut app, || asset_server.is_loaded(&handle));
120+
121+
let world = app.world_mut();
122+
b.iter(|| {
123+
for _ in 0..100 {
124+
world.spawn_scene(bsn! { :"a.bsn" }).unwrap();
125+
}
126+
});
127+
});
128+
group.finish();
129+
}
130+
131+
#[derive(Asset, TypePath)]
132+
struct EmptyAsset;
133+
134+
#[derive(Component, FromTemplate)]
135+
#[expect(unused, reason = "this is just used for init")]
136+
struct AssetReference(Handle<EmptyAsset>);
137+
138+
fn asset_value_scene() -> impl Scene {
139+
let children = (0..10)
140+
.map(|_| {
141+
bsn! {AssetReference(asset_value(EmptyAsset))}
142+
})
143+
.collect::<Vec<_>>();
144+
bsn! {
145+
Children [{children}]
146+
}
147+
}
148+
149+
fn asset_handle_scene(mut handles: Vec<Handle<EmptyAsset>>) -> impl Scene {
150+
let children = handles
151+
.drain(..)
152+
.map(|handle| {
153+
bsn! {AssetReference({handle.clone()})}
154+
})
155+
.collect::<Vec<_>>();
156+
bsn! {
157+
Children [{children}]
158+
}
159+
}
160+
20161
fn ui() -> impl Scene {
21162
bsn! {
22163
Node
@@ -209,88 +350,47 @@ fn run_app_until(app: &mut App, mut predicate: impl FnMut() -> bool) {
209350
panic!("Ran out of loops to return `Some` from `predicate`");
210351
}
211352

212-
fn spawn(c: &mut Criterion) {
213-
let mut group = c.benchmark_group("spawn");
214-
group.warm_up_time(Duration::from_millis(500));
215-
group.measurement_time(Duration::from_secs(4));
216-
group.bench_function("ui_immediate_function_scene", |b| {
217-
let mut app = App::new();
218-
app.add_plugins((bevy_asset::AssetPlugin::default(), bevy_scene::ScenePlugin));
219-
220-
b.iter(move || {
221-
app.world_mut().spawn_scene(ui()).unwrap();
222-
});
223-
});
224-
group.bench_function("ui_immediate_loaded_scene", |b| {
225-
let mut app = App::new();
226-
let dir = Dir::default();
227-
let dir_clone = dir.clone();
228-
app.register_asset_source(
229-
AssetSourceId::Default,
230-
AssetSourceBuilder::new(move || {
231-
Box::new(MemoryAssetReader {
232-
root: dir_clone.clone(),
233-
})
234-
}),
235-
);
236-
app.add_plugins((
237-
bevy_app::TaskPoolPlugin::default(),
238-
bevy_asset::AssetPlugin::default(),
239-
bevy_scene::ScenePlugin,
240-
));
241-
app.finish();
242-
app.cleanup();
243-
244-
// Create a fake loader to act as a ScenePatch loaded from a file.
245-
app.register_asset_loader(FakeSceneLoader);
246-
247-
#[derive(TypePath)]
248-
struct FakeSceneLoader;
249-
250-
impl AssetLoader for FakeSceneLoader {
251-
type Asset = ScenePatch;
252-
type Error = std::io::Error;
253-
type Settings = ();
254-
255-
async fn load(
256-
&self,
257-
_reader: &mut dyn bevy_asset::io::Reader,
258-
_settings: &Self::Settings,
259-
load_context: &mut bevy_asset::LoadContext<'_>,
260-
) -> Result<Self::Asset, Self::Error> {
261-
Ok(ScenePatch::load_with(load_context, button()))
262-
}
263-
}
264-
265-
// Insert an asset that the fake loader can fake read.
266-
dir.insert_asset_text(Path::new("button.bsn"), "");
267-
268-
let asset_server = app.world().resource::<AssetServer>().clone();
269-
let handle = asset_server.load("button.bsn");
270-
assert!(app.world().get_resource::<Assets<ScenePatch>>().is_some());
353+
fn bench_app(before: impl FnOnce(&mut App), after: impl FnOnce(&mut App)) -> App {
354+
let mut app = App::new();
355+
before(&mut app);
356+
app.add_plugins((
357+
bevy_app::TaskPoolPlugin::default(),
358+
bevy_asset::AssetPlugin::default(),
359+
bevy_scene::ScenePlugin,
360+
));
361+
after(&mut app);
362+
app.finish();
363+
app.cleanup();
364+
app
365+
}
271366

272-
run_app_until(&mut app, || asset_server.is_loaded(&handle));
367+
fn in_memory_asset_source(dir: Dir, app: &mut App) {
368+
app.register_asset_source(
369+
AssetSourceId::Default,
370+
AssetSourceBuilder::new(move || Box::new(MemoryAssetReader { root: dir.clone() })),
371+
);
372+
}
273373

274-
let patch = app
275-
.world()
276-
.resource::<Assets<ScenePatch>>()
277-
.get(&handle)
278-
.unwrap();
279-
assert!(patch.resolved.is_some());
374+
#[derive(TypePath)]
375+
struct FakeSceneLoader(Box<dyn Fn() -> Box<dyn Scene> + Send + Sync>);
280376

281-
b.iter(move || {
282-
app.world_mut().spawn_scene(ui_loaded_asset()).unwrap();
283-
});
377+
impl FakeSceneLoader {
378+
pub fn new<S: Scene>(scene_fn: impl (Fn() -> S) + Send + Sync + 'static) -> Self {
379+
Self(Box::new(move || Box::new(scene_fn())))
380+
}
381+
}
284382

285-
drop(handle);
286-
});
287-
group.bench_function("ui_raw_bundle_no_scene", |b| {
288-
let mut app = App::new();
289-
app.add_plugins((bevy_asset::AssetPlugin::default(), bevy_scene::ScenePlugin));
383+
impl AssetLoader for FakeSceneLoader {
384+
type Asset = ScenePatch;
385+
type Error = std::io::Error;
386+
type Settings = ();
290387

291-
b.iter(move || {
292-
app.world_mut().spawn(raw_ui());
293-
});
294-
});
295-
group.finish();
388+
async fn load(
389+
&self,
390+
_reader: &mut dyn bevy_asset::io::Reader,
391+
_settings: &Self::Settings,
392+
load_context: &mut bevy_asset::LoadContext<'_>,
393+
) -> Result<Self::Asset, Self::Error> {
394+
Ok(ScenePatch::load_with(load_context, (self.0)()))
395+
}
296396
}

crates/bevy_asset/src/handle.rs

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use crate::{
44
};
55
use alloc::sync::Arc;
66
use bevy_ecs::template::{FromTemplate, SpecializeFromTemplate, Template, TemplateContext};
7-
use bevy_platform::collections::Equivalent;
7+
use bevy_platform::{collections::Equivalent, sync::Mutex};
88
use bevy_reflect::{Reflect, TypePath};
99
use core::{
1010
any::TypeId,
@@ -208,10 +208,56 @@ impl<T: Asset> FromTemplate for Handle<T> {
208208
type Template = HandleTemplate<T>;
209209
}
210210

211+
/// A [`Template`] that produces a [`Handle`].
211212
#[derive(Reflect)]
212213
pub enum HandleTemplate<T: Asset> {
214+
/// Creates a [`Handle`] by calling [`AssetServer::load`] on the given [`AssetPath`].
213215
Path(AssetPath<'static>),
216+
/// Creates a [`Handle`] by cloning the given [`Handle`] value.
214217
Handle(Handle<T>),
218+
/// Creates a [`Handle`] by adding the given asset value using [`AssetServer::add`]. This will
219+
/// cache the resulting [`Handle`] on the template and reuse it for future template builds.
220+
///
221+
/// This should generally be constructed using [`HandleTemplate::value`] or [`asset_value`].
222+
Value(ArcMutexValue<T>),
223+
}
224+
225+
impl<T: Asset> HandleTemplate<T> {
226+
/// This will create a new [`HandleTemplate`] for the given `asset` value. This makes it possible
227+
/// to define assets "inline" in templates / scenes that produce a [`Handle`].
228+
///
229+
/// This supports [`Into`]
230+
/// to automatically convert values that can become `A`.
231+
pub fn value(value: impl Into<T>) -> Self {
232+
HandleTemplate::Value(ArcMutexValue(Arc::new(Mutex::new(AssetOrHandle::Value(
233+
Some(value.into()),
234+
)))))
235+
}
236+
}
237+
238+
/// Stores an [`Arc<Mutex<AssetOrHandle<T>>>`].
239+
///
240+
/// This intermediary type exists largely to enable reflect(opaque).
241+
#[derive(Reflect)]
242+
#[reflect(opaque)]
243+
pub struct ArcMutexValue<T: Asset>(Arc<Mutex<AssetOrHandle<T>>>);
244+
245+
impl<T: Asset> Clone for ArcMutexValue<T> {
246+
fn clone(&self) -> Self {
247+
Self(self.0.clone())
248+
}
249+
}
250+
251+
#[derive(Reflect)]
252+
enum AssetOrHandle<T: Asset> {
253+
Value(Option<T>),
254+
Handle(Handle<T>),
255+
}
256+
257+
impl<T: Asset> Default for AssetOrHandle<T> {
258+
fn default() -> Self {
259+
Self::Handle(Default::default())
260+
}
215261
}
216262

217263
impl<T: Asset> Default for HandleTemplate<T> {
@@ -238,16 +284,42 @@ impl<T: Asset> Template for HandleTemplate<T> {
238284
Ok(match self {
239285
HandleTemplate::Path(asset_path) => context.resource::<AssetServer>().load(asset_path),
240286
HandleTemplate::Handle(handle) => handle.clone(),
287+
HandleTemplate::Value(value) => {
288+
// This unwrap is ok. If another caller panicked while holding this mutex, then the
289+
// program is in an invalid state and this should panic too.
290+
let mut value_or_handle = value.0.lock().unwrap();
291+
match &mut *value_or_handle {
292+
AssetOrHandle::Value(value) => {
293+
// This unwrap is ok because AssetOrHandle::Value will always either contain a Some Value
294+
// when it is in this state (AssetOrHandle is private).
295+
let handle = context.resource::<AssetServer>().add(value.take().unwrap());
296+
*value_or_handle = AssetOrHandle::Handle(handle.clone());
297+
handle
298+
}
299+
AssetOrHandle::Handle(handle) => handle.clone(),
300+
}
301+
}
241302
})
242303
}
243304

244305
fn clone_template(&self) -> Self {
245306
match self {
246307
HandleTemplate::Path(asset_path) => HandleTemplate::Path(asset_path.clone()),
247308
HandleTemplate::Handle(handle) => HandleTemplate::Handle(handle.clone()),
309+
HandleTemplate::Value(value) => HandleTemplate::Value(value.clone()),
248310
}
249311
}
250312
}
313+
314+
/// This will create a new [`HandleTemplate`] for the given `asset` value. This makes it possible
315+
/// to define assets "inline" in templates / scenes that produce a [`Handle`].
316+
///
317+
/// This supports [`Into`]
318+
/// to automatically convert values that can become `A`.
319+
pub fn asset_value<I: Into<A>, A: Asset>(asset: I) -> HandleTemplate<A> {
320+
HandleTemplate::value(asset)
321+
}
322+
251323
impl<A: Asset> core::fmt::Debug for Handle<A> {
252324
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
253325
let name = ShortName::of::<A>();

0 commit comments

Comments
 (0)