bevy_render/
render_asset.rs

1use crate::{
2    render_resource::AsBindGroupError, Extract, ExtractSchedule, MainWorld, Render, RenderApp,
3    RenderSystems, Res,
4};
5use bevy_app::{App, Plugin, SubApp};
6use bevy_asset::{Asset, AssetEvent, AssetId, Assets, RenderAssetUsages};
7use 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};
13use bevy_platform::collections::{HashMap, HashSet};
14use core::marker::PhantomData;
15use core::sync::atomic::{AtomicUsize, Ordering};
16use thiserror::Error;
17use tracing::{debug, error};
18
19#[derive(Debug, Error)]
20pub 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)]
29pub struct AssetExtractionSystems;
30
31/// Error returned when an asset due for extraction has already been extracted
32#[derive(Debug, Error)]
33pub 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`].
47pub 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()`.
118pub struct RenderAssetPlugin<A: RenderAsset, AFTER: RenderAssetDependency + 'static = ()> {
119    phantom: PhantomData<fn() -> (A, AFTER)>,
120}
121
122impl<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
132impl<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
155pub trait RenderAssetDependency {
156    fn register_system(render_app: &mut SubApp, system: ScheduleConfigs<ScheduleSystem>);
157}
158
159impl RenderAssetDependency for () {
160    fn register_system(render_app: &mut SubApp, system: ScheduleConfigs<ScheduleSystem>) {
161        render_app.add_systems(Render, system);
162    }
163}
164
165impl<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)]
173pub 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
191impl<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)]
205pub struct RenderAssets<A: RenderAsset>(HashMap<AssetId<A::SourceAsset>, A>);
206
207impl<A: RenderAsset> Default for RenderAssets<A> {
208    fn default() -> Self {
209        Self(Default::default())
210    }
211}
212
213impl<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)]
240struct 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
248impl<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".
258pub(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)]
339pub struct PrepareNextFrameAssets<A: RenderAsset> {
340    assets: Vec<(AssetId<A::SourceAsset>, A::SourceAsset)>,
341}
342
343impl<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.
353pub 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
452pub fn reset_render_asset_bytes_per_frame(
453    mut bpf_limiter: ResMut<RenderAssetBytesPerFrameLimiter>,
454) {
455    bpf_limiter.reset();
456}
457
458pub 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)]
469pub struct RenderAssetBytesPerFrame {
470    pub max_bytes: Option<usize>,
471}
472
473impl 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)]
492pub 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
499impl 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}