Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_render/src/erased_render_asset.rs
9395 views
1
use crate::{
2
render_resource::AsBindGroupError, ExtractSchedule, MainWorld, Render, RenderApp,
3
RenderSystems, Res,
4
};
5
use bevy_app::{App, Plugin, SubApp};
6
use bevy_asset::RenderAssetUsages;
7
use bevy_asset::{Asset, AssetEvent, AssetId, Assets, UntypedAssetId};
8
use bevy_ecs::{
9
prelude::{Commands, IntoScheduleConfigs, MessageReader, ResMut, Resource},
10
schedule::{ScheduleConfigs, SystemSet},
11
system::{ScheduleSystem, StaticSystemParam, SystemParam, SystemParamItem, SystemState},
12
world::{FromWorld, Mut},
13
};
14
use bevy_log::{debug, error};
15
use bevy_platform::collections::{HashMap, HashSet};
16
use bevy_render::render_asset::RenderAssetBytesPerFrameLimiter;
17
use core::marker::PhantomData;
18
use thiserror::Error;
19
20
#[derive(Debug, Error)]
21
pub enum PrepareAssetError<E: Send + Sync + 'static> {
22
#[error("Failed to prepare asset")]
23
RetryNextUpdate(E),
24
#[error("Failed to build bind group: {0}")]
25
AsBindGroupError(AsBindGroupError),
26
}
27
28
/// The system set during which we extract modified assets to the render world.
29
#[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)]
30
pub struct AssetExtractionSystems;
31
32
/// Describes how an asset gets extracted and prepared for rendering.
33
///
34
/// In the [`ExtractSchedule`] step the [`ErasedRenderAsset::SourceAsset`] is transferred
35
/// from the "main world" into the "render world".
36
///
37
/// After that in the [`RenderSystems::PrepareAssets`] step the extracted asset
38
/// is transformed into its GPU-representation of type [`ErasedRenderAsset`].
39
pub trait ErasedRenderAsset: Send + Sync + 'static {
40
/// The representation of the asset in the "main world".
41
type SourceAsset: Asset + Clone;
42
/// The target representation of the asset in the "render world".
43
type ErasedAsset: Send + Sync + 'static + Sized;
44
45
/// Specifies all ECS data required by [`ErasedRenderAsset::prepare_asset`].
46
///
47
/// For convenience use the [`lifetimeless`](bevy_ecs::system::lifetimeless) [`SystemParam`].
48
type Param: SystemParam;
49
50
/// Whether or not to unload the asset after extracting it to the render world.
51
#[inline]
52
fn asset_usage(_source_asset: &Self::SourceAsset) -> RenderAssetUsages {
53
RenderAssetUsages::default()
54
}
55
56
/// Size of the data the asset will upload to the gpu. Specifying a return value
57
/// will allow the asset to be throttled via [`RenderAssetBytesPerFrameLimiter`].
58
#[inline]
59
#[expect(
60
unused_variables,
61
reason = "The parameters here are intentionally unused by the default implementation; however, putting underscores here will result in the underscores being copied by rust-analyzer's tab completion."
62
)]
63
fn byte_len(erased_asset: &Self::SourceAsset) -> Option<usize> {
64
None
65
}
66
67
/// Prepares the [`ErasedRenderAsset::SourceAsset`] for the GPU by transforming it into a [`ErasedRenderAsset`].
68
///
69
/// ECS data may be accessed via `param`.
70
fn prepare_asset(
71
source_asset: Self::SourceAsset,
72
asset_id: AssetId<Self::SourceAsset>,
73
param: &mut SystemParamItem<Self::Param>,
74
) -> Result<Self::ErasedAsset, PrepareAssetError<Self::SourceAsset>>;
75
76
/// Called whenever the [`ErasedRenderAsset::SourceAsset`] has been removed.
77
///
78
/// You can implement this method if you need to access ECS data (via
79
/// `_param`) in order to perform cleanup tasks when the asset is removed.
80
///
81
/// The default implementation does nothing.
82
fn unload_asset(
83
_source_asset: AssetId<Self::SourceAsset>,
84
_param: &mut SystemParamItem<Self::Param>,
85
) {
86
}
87
}
88
89
/// This plugin extracts the changed assets from the "app world" into the "render world"
90
/// and prepares them for the GPU. They can then be accessed from the [`ErasedRenderAssets`] resource.
91
///
92
/// Therefore it sets up the [`ExtractSchedule`] and
93
/// [`RenderSystems::PrepareAssets`] steps for the specified [`ErasedRenderAsset`].
94
///
95
/// The `AFTER` generic parameter can be used to specify that `A::prepare_asset` should not be run until
96
/// `prepare_assets::<AFTER>` has completed. This allows the `prepare_asset` function to depend on another
97
/// prepared [`ErasedRenderAsset`], for example `Mesh::prepare_asset` relies on `ErasedRenderAssets::<GpuImage>` for morph
98
/// targets, so the plugin is created as `ErasedRenderAssetPlugin::<RenderMesh, GpuImage>::default()`.
99
pub struct ErasedRenderAssetPlugin<
100
A: ErasedRenderAsset,
101
AFTER: ErasedRenderAssetDependency + 'static = (),
102
> {
103
phantom: PhantomData<fn() -> (A, AFTER)>,
104
}
105
106
impl<A: ErasedRenderAsset, AFTER: ErasedRenderAssetDependency + 'static> Default
107
for ErasedRenderAssetPlugin<A, AFTER>
108
{
109
fn default() -> Self {
110
Self {
111
phantom: Default::default(),
112
}
113
}
114
}
115
116
impl<A: ErasedRenderAsset, AFTER: ErasedRenderAssetDependency + 'static> Plugin
117
for ErasedRenderAssetPlugin<A, AFTER>
118
{
119
fn build(&self, app: &mut App) {
120
app.init_resource::<CachedExtractErasedRenderAssetSystemState<A>>();
121
}
122
123
fn finish(&self, app: &mut App) {
124
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
125
render_app
126
.init_resource::<ExtractedAssets<A>>()
127
.init_resource::<ErasedRenderAssets<A::ErasedAsset>>()
128
.init_resource::<PrepareNextFrameAssets<A>>()
129
.add_systems(
130
ExtractSchedule,
131
extract_erased_render_asset::<A>.in_set(AssetExtractionSystems),
132
);
133
AFTER::register_system(
134
render_app,
135
prepare_erased_assets::<A>.in_set(RenderSystems::PrepareAssets),
136
);
137
}
138
}
139
}
140
141
// helper to allow specifying dependencies between render assets
142
pub trait ErasedRenderAssetDependency {
143
fn register_system(render_app: &mut SubApp, system: ScheduleConfigs<ScheduleSystem>);
144
}
145
146
impl ErasedRenderAssetDependency for () {
147
fn register_system(render_app: &mut SubApp, system: ScheduleConfigs<ScheduleSystem>) {
148
render_app.add_systems(Render, system);
149
}
150
}
151
152
impl<A: ErasedRenderAsset> ErasedRenderAssetDependency for A {
153
fn register_system(render_app: &mut SubApp, system: ScheduleConfigs<ScheduleSystem>) {
154
render_app.add_systems(Render, system.after(prepare_erased_assets::<A>));
155
}
156
}
157
158
/// Temporarily stores the extracted and removed assets of the current frame.
159
#[derive(Resource)]
160
pub struct ExtractedAssets<A: ErasedRenderAsset> {
161
/// The assets extracted this frame.
162
///
163
/// These are assets that were either added or modified this frame.
164
pub extracted: Vec<(AssetId<A::SourceAsset>, A::SourceAsset)>,
165
166
/// IDs of the assets that were removed this frame.
167
///
168
/// These assets will not be present in [`ExtractedAssets::extracted`].
169
pub removed: HashSet<AssetId<A::SourceAsset>>,
170
171
/// IDs of the assets that were modified this frame.
172
pub modified: HashSet<AssetId<A::SourceAsset>>,
173
174
/// IDs of the assets that were added this frame.
175
pub added: HashSet<AssetId<A::SourceAsset>>,
176
}
177
178
impl<A: ErasedRenderAsset> Default for ExtractedAssets<A> {
179
fn default() -> Self {
180
Self {
181
extracted: Default::default(),
182
removed: Default::default(),
183
modified: Default::default(),
184
added: Default::default(),
185
}
186
}
187
}
188
189
/// Stores all GPU representations ([`ErasedRenderAsset`])
190
/// of [`ErasedRenderAsset::SourceAsset`] as long as they exist.
191
#[derive(Resource)]
192
pub struct ErasedRenderAssets<ERA>(HashMap<UntypedAssetId, ERA>);
193
194
impl<ERA> Default for ErasedRenderAssets<ERA> {
195
fn default() -> Self {
196
Self(Default::default())
197
}
198
}
199
200
impl<ERA> ErasedRenderAssets<ERA> {
201
pub fn get(&self, id: impl Into<UntypedAssetId>) -> Option<&ERA> {
202
self.0.get(&id.into())
203
}
204
205
pub fn get_mut(&mut self, id: impl Into<UntypedAssetId>) -> Option<&mut ERA> {
206
self.0.get_mut(&id.into())
207
}
208
209
pub fn insert(&mut self, id: impl Into<UntypedAssetId>, value: ERA) -> Option<ERA> {
210
self.0.insert(id.into(), value)
211
}
212
213
pub fn remove(&mut self, id: impl Into<UntypedAssetId>) -> Option<ERA> {
214
self.0.remove(&id.into())
215
}
216
217
pub fn iter(&self) -> impl Iterator<Item = (UntypedAssetId, &ERA)> {
218
self.0.iter().map(|(k, v)| (*k, v))
219
}
220
221
pub fn iter_mut(&mut self) -> impl Iterator<Item = (UntypedAssetId, &mut ERA)> {
222
self.0.iter_mut().map(|(k, v)| (*k, v))
223
}
224
}
225
226
#[derive(Resource)]
227
struct CachedExtractErasedRenderAssetSystemState<A: ErasedRenderAsset> {
228
state: SystemState<(
229
MessageReader<'static, 'static, AssetEvent<A::SourceAsset>>,
230
ResMut<'static, Assets<A::SourceAsset>>,
231
)>,
232
}
233
234
impl<A: ErasedRenderAsset> FromWorld for CachedExtractErasedRenderAssetSystemState<A> {
235
fn from_world(world: &mut bevy_ecs::world::World) -> Self {
236
Self {
237
state: SystemState::new(world),
238
}
239
}
240
}
241
242
/// This system extracts all created or modified assets of the corresponding [`ErasedRenderAsset::SourceAsset`] type
243
/// into the "render world".
244
pub(crate) fn extract_erased_render_asset<A: ErasedRenderAsset>(
245
mut commands: Commands,
246
mut main_world: ResMut<MainWorld>,
247
) {
248
main_world.resource_scope(
249
|world, mut cached_state: Mut<CachedExtractErasedRenderAssetSystemState<A>>| {
250
let (mut events, mut assets) = cached_state.state.get_mut(world);
251
252
let mut needs_extracting = <HashSet<_>>::default();
253
let mut removed = <HashSet<_>>::default();
254
let mut modified = <HashSet<_>>::default();
255
256
for event in events.read() {
257
#[expect(
258
clippy::match_same_arms,
259
reason = "LoadedWithDependencies is marked as a TODO, so it's likely this will no longer lint soon."
260
)]
261
match event {
262
AssetEvent::Added { id } => {
263
needs_extracting.insert(*id);
264
}
265
AssetEvent::Modified { id } => {
266
needs_extracting.insert(*id);
267
modified.insert(*id);
268
}
269
AssetEvent::Removed { .. } => {
270
// We don't care that the asset was removed from Assets<T> in the main world.
271
// An asset is only removed from ErasedRenderAssets<T> when its last handle is dropped (AssetEvent::Unused).
272
}
273
AssetEvent::Unused { id } => {
274
needs_extracting.remove(id);
275
modified.remove(id);
276
removed.insert(*id);
277
}
278
AssetEvent::LoadedWithDependencies { .. } => {
279
// TODO: handle this
280
}
281
}
282
}
283
284
let mut extracted_assets = Vec::new();
285
let mut added = <HashSet<_>>::default();
286
for id in needs_extracting.drain() {
287
if let Some(asset) = assets.get(id) {
288
let asset_usage = A::asset_usage(asset);
289
if asset_usage.contains(RenderAssetUsages::RENDER_WORLD) {
290
if asset_usage == RenderAssetUsages::RENDER_WORLD {
291
if let Some(asset) = assets.remove(id) {
292
extracted_assets.push((id, asset));
293
added.insert(id);
294
}
295
} else {
296
extracted_assets.push((id, asset.clone()));
297
added.insert(id);
298
}
299
}
300
}
301
}
302
303
commands.insert_resource(ExtractedAssets::<A> {
304
extracted: extracted_assets,
305
removed,
306
modified,
307
added,
308
});
309
cached_state.state.apply(world);
310
},
311
);
312
}
313
314
// TODO: consider storing inside system?
315
/// All assets that should be prepared next frame.
316
#[derive(Resource)]
317
pub struct PrepareNextFrameAssets<A: ErasedRenderAsset> {
318
assets: Vec<(AssetId<A::SourceAsset>, A::SourceAsset)>,
319
}
320
321
impl<A: ErasedRenderAsset> Default for PrepareNextFrameAssets<A> {
322
fn default() -> Self {
323
Self {
324
assets: Default::default(),
325
}
326
}
327
}
328
329
/// This system prepares all assets of the corresponding [`ErasedRenderAsset::SourceAsset`] type
330
/// which where extracted this frame for the GPU.
331
pub fn prepare_erased_assets<A: ErasedRenderAsset>(
332
mut extracted_assets: ResMut<ExtractedAssets<A>>,
333
mut render_assets: ResMut<ErasedRenderAssets<A::ErasedAsset>>,
334
mut prepare_next_frame: ResMut<PrepareNextFrameAssets<A>>,
335
param: StaticSystemParam<<A as ErasedRenderAsset>::Param>,
336
bpf: Res<RenderAssetBytesPerFrameLimiter>,
337
) {
338
let mut wrote_asset_count = 0;
339
340
let mut param = param.into_inner();
341
let queued_assets = core::mem::take(&mut prepare_next_frame.assets);
342
for (id, extracted_asset) in queued_assets {
343
if extracted_assets.removed.contains(&id) || extracted_assets.added.contains(&id) {
344
// skip previous frame's assets that have been removed or updated
345
continue;
346
}
347
348
let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) {
349
// we could check if available bytes > byte_len here, but we want to make some
350
// forward progress even if the asset is larger than the max bytes per frame.
351
// this way we always write at least one (sized) asset per frame.
352
// in future we could also consider partial asset uploads.
353
if bpf.exhausted() {
354
prepare_next_frame.assets.push((id, extracted_asset));
355
continue;
356
}
357
size
358
} else {
359
0
360
};
361
362
match A::prepare_asset(extracted_asset, id, &mut param) {
363
Ok(prepared_asset) => {
364
render_assets.insert(id, prepared_asset);
365
bpf.write_bytes(write_bytes);
366
wrote_asset_count += 1;
367
}
368
Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => {
369
prepare_next_frame.assets.push((id, extracted_asset));
370
}
371
Err(PrepareAssetError::AsBindGroupError(e)) => {
372
error!(
373
"{} Bind group construction failed: {e}",
374
core::any::type_name::<A>()
375
);
376
}
377
}
378
}
379
380
for removed in extracted_assets.removed.drain() {
381
render_assets.remove(removed);
382
A::unload_asset(removed, &mut param);
383
}
384
385
for (id, extracted_asset) in extracted_assets.extracted.drain(..) {
386
// we remove previous here to ensure that if we are updating the asset then
387
// any users will not see the old asset after a new asset is extracted,
388
// even if the new asset is not yet ready or we are out of bytes to write.
389
render_assets.remove(id);
390
391
let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) {
392
if bpf.exhausted() {
393
prepare_next_frame.assets.push((id, extracted_asset));
394
continue;
395
}
396
size
397
} else {
398
0
399
};
400
401
match A::prepare_asset(extracted_asset, id, &mut param) {
402
Ok(prepared_asset) => {
403
render_assets.insert(id, prepared_asset);
404
bpf.write_bytes(write_bytes);
405
wrote_asset_count += 1;
406
}
407
Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => {
408
prepare_next_frame.assets.push((id, extracted_asset));
409
}
410
Err(PrepareAssetError::AsBindGroupError(e)) => {
411
error!(
412
"{} Bind group construction failed: {e}",
413
core::any::type_name::<A>()
414
);
415
}
416
}
417
}
418
419
if bpf.exhausted() && !prepare_next_frame.assets.is_empty() {
420
debug!(
421
"{} write budget exhausted with {} assets remaining (wrote {})",
422
core::any::type_name::<A>(),
423
prepare_next_frame.assets.len(),
424
wrote_asset_count
425
);
426
}
427
}
428
429