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