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