bevy_render/
render_asset.rs

1use crate::{ExtractSchedule, MainWorld, Render, RenderApp, RenderSet};
2use bevy_app::{App, Plugin, SubApp};
3use bevy_asset::{Asset, AssetEvent, AssetId, Assets};
4use bevy_ecs::{
5    prelude::{Commands, EventReader, IntoSystemConfigs, ResMut, Resource},
6    schedule::SystemConfigs,
7    system::{StaticSystemParam, SystemParam, SystemParamItem, SystemState},
8    world::{FromWorld, Mut},
9};
10use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
11use bevy_render_macros::ExtractResource;
12use bevy_utils::{tracing::debug, HashMap, HashSet};
13use serde::{Deserialize, Serialize};
14use std::marker::PhantomData;
15use thiserror::Error;
16
17#[derive(Debug, Error)]
18pub enum PrepareAssetError<E: Send + Sync + 'static> {
19    #[error("Failed to prepare asset")]
20    RetryNextUpdate(E),
21}
22
23/// Describes how an asset gets extracted and prepared for rendering.
24///
25/// In the [`ExtractSchedule`] step the [`RenderAsset::SourceAsset`] is transferred
26/// from the "main world" into the "render world".
27///
28/// After that in the [`RenderSet::PrepareAssets`] step the extracted asset
29/// is transformed into its GPU-representation of type [`RenderAsset`].
30pub trait RenderAsset: Send + Sync + 'static + Sized {
31    /// The representation of the asset in the "main world".
32    type SourceAsset: Asset + Clone;
33
34    /// Specifies all ECS data required by [`RenderAsset::prepare_asset`].
35    ///
36    /// For convenience use the [`lifetimeless`](bevy_ecs::system::lifetimeless) [`SystemParam`].
37    type Param: SystemParam;
38
39    /// Whether or not to unload the asset after extracting it to the render world.
40    #[inline]
41    fn asset_usage(_source_asset: &Self::SourceAsset) -> RenderAssetUsages {
42        RenderAssetUsages::default()
43    }
44
45    /// Size of the data the asset will upload to the gpu. Specifying a return value
46    /// will allow the asset to be throttled via [`RenderAssetBytesPerFrame`].
47    #[inline]
48    #[allow(unused_variables)]
49    fn byte_len(source_asset: &Self::SourceAsset) -> Option<usize> {
50        None
51    }
52
53    /// Prepares the [`RenderAsset::SourceAsset`] for the GPU by transforming it into a [`RenderAsset`].
54    ///
55    /// ECS data may be accessed via `param`.
56    fn prepare_asset(
57        source_asset: Self::SourceAsset,
58        param: &mut SystemParamItem<Self::Param>,
59    ) -> Result<Self, PrepareAssetError<Self::SourceAsset>>;
60}
61
62bitflags::bitflags! {
63    /// Defines where the asset will be used.
64    ///
65    /// If an asset is set to the `RENDER_WORLD` but not the `MAIN_WORLD`, the asset will be
66    /// unloaded from the asset server once it's been extracted and prepared in the render world.
67    ///
68    /// Unloading the asset saves on memory, as for most cases it is no longer necessary to keep
69    /// it in RAM once it's been uploaded to the GPU's VRAM. However, this means you can no longer
70    /// access the asset from the CPU (via the `Assets<T>` resource) once unloaded (without re-loading it).
71    ///
72    /// If you never need access to the asset from the CPU past the first frame it's loaded on,
73    /// or only need very infrequent access, then set this to `RENDER_WORLD`. Otherwise, set this to
74    /// `RENDER_WORLD | MAIN_WORLD`.
75    ///
76    /// If you have an asset that doesn't actually need to end up in the render world, like an Image
77    /// that will be decoded into another Image asset, use `MAIN_WORLD` only.
78    ///
79    /// ## Platform-specific
80    ///
81    /// On Wasm, it is not possible for now to free reserved memory. To control memory usage, load assets
82    /// in sequence and unload one before loading the next. See this
83    /// [discussion about memory management](https://github.com/WebAssembly/design/issues/1397) for more
84    /// details.
85    #[repr(transparent)]
86    #[derive(Serialize, Deserialize, Hash, Clone, Copy, PartialEq, Eq, Debug, Reflect)]
87    #[reflect_value(Serialize, Deserialize, Hash, PartialEq, Debug)]
88    pub struct RenderAssetUsages: u8 {
89        const MAIN_WORLD = 1 << 0;
90        const RENDER_WORLD = 1 << 1;
91    }
92}
93
94impl Default for RenderAssetUsages {
95    /// Returns the default render asset usage flags:
96    /// `RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD`
97    ///
98    /// This default configuration ensures the asset persists in the main world, even after being prepared for rendering.
99    ///
100    /// If your asset does not change, consider using `RenderAssetUsages::RENDER_WORLD` exclusively. This will cause
101    /// the asset to be unloaded from the main world once it has been prepared for rendering. If the asset does not need
102    /// to reach the render world at all, use `RenderAssetUsages::MAIN_WORLD` exclusively.
103    fn default() -> Self {
104        RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD
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/// [`RenderSet::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::<GpuMesh, 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(ExtractSchedule, extract_render_asset::<A>);
143            AFTER::register_system(
144                render_app,
145                prepare_assets::<A>.in_set(RenderSet::PrepareAssets),
146            );
147        }
148    }
149}
150
151// helper to allow specifying dependencies between render assets
152pub trait RenderAssetDependency {
153    fn register_system(render_app: &mut SubApp, system: SystemConfigs);
154}
155
156impl RenderAssetDependency for () {
157    fn register_system(render_app: &mut SubApp, system: SystemConfigs) {
158        render_app.add_systems(Render, system);
159    }
160}
161
162impl<A: RenderAsset> RenderAssetDependency for A {
163    fn register_system(render_app: &mut SubApp, system: SystemConfigs) {
164        render_app.add_systems(Render, system.after(prepare_assets::<A>));
165    }
166}
167
168/// Temporarily stores the extracted and removed assets of the current frame.
169#[derive(Resource)]
170pub struct ExtractedAssets<A: RenderAsset> {
171    extracted: Vec<(AssetId<A::SourceAsset>, A::SourceAsset)>,
172    removed: HashSet<AssetId<A::SourceAsset>>,
173    added: HashSet<AssetId<A::SourceAsset>>,
174}
175
176impl<A: RenderAsset> Default for ExtractedAssets<A> {
177    fn default() -> Self {
178        Self {
179            extracted: Default::default(),
180            removed: Default::default(),
181            added: Default::default(),
182        }
183    }
184}
185
186/// Stores all GPU representations ([`RenderAsset`])
187/// of [`RenderAsset::SourceAsset`] as long as they exist.
188#[derive(Resource)]
189pub struct RenderAssets<A: RenderAsset>(HashMap<AssetId<A::SourceAsset>, A>);
190
191impl<A: RenderAsset> Default for RenderAssets<A> {
192    fn default() -> Self {
193        Self(Default::default())
194    }
195}
196
197impl<A: RenderAsset> RenderAssets<A> {
198    pub fn get(&self, id: impl Into<AssetId<A::SourceAsset>>) -> Option<&A> {
199        self.0.get(&id.into())
200    }
201
202    pub fn get_mut(&mut self, id: impl Into<AssetId<A::SourceAsset>>) -> Option<&mut A> {
203        self.0.get_mut(&id.into())
204    }
205
206    pub fn insert(&mut self, id: impl Into<AssetId<A::SourceAsset>>, value: A) -> Option<A> {
207        self.0.insert(id.into(), value)
208    }
209
210    pub fn remove(&mut self, id: impl Into<AssetId<A::SourceAsset>>) -> Option<A> {
211        self.0.remove(&id.into())
212    }
213
214    pub fn iter(&self) -> impl Iterator<Item = (AssetId<A::SourceAsset>, &A)> {
215        self.0.iter().map(|(k, v)| (*k, v))
216    }
217
218    pub fn iter_mut(&mut self) -> impl Iterator<Item = (AssetId<A::SourceAsset>, &mut A)> {
219        self.0.iter_mut().map(|(k, v)| (*k, v))
220    }
221}
222
223#[derive(Resource)]
224struct CachedExtractRenderAssetSystemState<A: RenderAsset> {
225    state: SystemState<(
226        EventReader<'static, 'static, AssetEvent<A::SourceAsset>>,
227        ResMut<'static, Assets<A::SourceAsset>>,
228    )>,
229}
230
231impl<A: RenderAsset> FromWorld for CachedExtractRenderAssetSystemState<A> {
232    fn from_world(world: &mut bevy_ecs::world::World) -> Self {
233        Self {
234            state: SystemState::new(world),
235        }
236    }
237}
238
239/// This system extracts all created or modified assets of the corresponding [`RenderAsset::SourceAsset`] type
240/// into the "render world".
241fn extract_render_asset<A: RenderAsset>(mut commands: Commands, mut main_world: ResMut<MainWorld>) {
242    main_world.resource_scope(
243        |world, mut cached_state: Mut<CachedExtractRenderAssetSystemState<A>>| {
244            let (mut events, mut assets) = cached_state.state.get_mut(world);
245
246            let mut changed_assets = HashSet::default();
247            let mut removed = HashSet::default();
248
249            for event in events.read() {
250                #[allow(clippy::match_same_arms)]
251                match event {
252                    AssetEvent::Added { id } | AssetEvent::Modified { id } => {
253                        changed_assets.insert(*id);
254                    }
255                    AssetEvent::Removed { .. } => {}
256                    AssetEvent::Unused { id } => {
257                        changed_assets.remove(id);
258                        removed.insert(*id);
259                    }
260                    AssetEvent::LoadedWithDependencies { .. } => {
261                        // TODO: handle this
262                    }
263                }
264            }
265
266            let mut extracted_assets = Vec::new();
267            let mut added = HashSet::new();
268            for id in changed_assets.drain() {
269                if let Some(asset) = assets.get(id) {
270                    let asset_usage = A::asset_usage(asset);
271                    if asset_usage.contains(RenderAssetUsages::RENDER_WORLD) {
272                        if asset_usage == RenderAssetUsages::RENDER_WORLD {
273                            if let Some(asset) = assets.remove(id) {
274                                extracted_assets.push((id, asset));
275                                added.insert(id);
276                            }
277                        } else {
278                            extracted_assets.push((id, asset.clone()));
279                            added.insert(id);
280                        }
281                    }
282                }
283            }
284
285            commands.insert_resource(ExtractedAssets::<A> {
286                extracted: extracted_assets,
287                removed,
288                added,
289            });
290            cached_state.state.apply(world);
291        },
292    );
293}
294
295// TODO: consider storing inside system?
296/// All assets that should be prepared next frame.
297#[derive(Resource)]
298pub struct PrepareNextFrameAssets<A: RenderAsset> {
299    assets: Vec<(AssetId<A::SourceAsset>, A::SourceAsset)>,
300}
301
302impl<A: RenderAsset> Default for PrepareNextFrameAssets<A> {
303    fn default() -> Self {
304        Self {
305            assets: Default::default(),
306        }
307    }
308}
309
310/// This system prepares all assets of the corresponding [`RenderAsset::SourceAsset`] type
311/// which where extracted this frame for the GPU.
312pub fn prepare_assets<A: RenderAsset>(
313    mut extracted_assets: ResMut<ExtractedAssets<A>>,
314    mut render_assets: ResMut<RenderAssets<A>>,
315    mut prepare_next_frame: ResMut<PrepareNextFrameAssets<A>>,
316    param: StaticSystemParam<<A as RenderAsset>::Param>,
317    mut bpf: ResMut<RenderAssetBytesPerFrame>,
318) {
319    let mut wrote_asset_count = 0;
320
321    let mut param = param.into_inner();
322    let queued_assets = std::mem::take(&mut prepare_next_frame.assets);
323    for (id, extracted_asset) in queued_assets {
324        if extracted_assets.removed.contains(&id) || extracted_assets.added.contains(&id) {
325            // skip previous frame's assets that have been removed or updated
326            continue;
327        }
328
329        let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) {
330            // we could check if available bytes > byte_len here, but we want to make some
331            // forward progress even if the asset is larger than the max bytes per frame.
332            // this way we always write at least one (sized) asset per frame.
333            // in future we could also consider partial asset uploads.
334            if bpf.exhausted() {
335                prepare_next_frame.assets.push((id, extracted_asset));
336                continue;
337            }
338            size
339        } else {
340            0
341        };
342
343        match A::prepare_asset(extracted_asset, &mut param) {
344            Ok(prepared_asset) => {
345                render_assets.insert(id, prepared_asset);
346                bpf.write_bytes(write_bytes);
347                wrote_asset_count += 1;
348            }
349            Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => {
350                prepare_next_frame.assets.push((id, extracted_asset));
351            }
352        }
353    }
354
355    for removed in extracted_assets.removed.drain() {
356        render_assets.remove(removed);
357    }
358
359    for (id, extracted_asset) in extracted_assets.extracted.drain(..) {
360        // we remove previous here to ensure that if we are updating the asset then
361        // any users will not see the old asset after a new asset is extracted,
362        // even if the new asset is not yet ready or we are out of bytes to write.
363        render_assets.remove(id);
364
365        let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) {
366            if bpf.exhausted() {
367                prepare_next_frame.assets.push((id, extracted_asset));
368                continue;
369            }
370            size
371        } else {
372            0
373        };
374
375        match A::prepare_asset(extracted_asset, &mut param) {
376            Ok(prepared_asset) => {
377                render_assets.insert(id, prepared_asset);
378                bpf.write_bytes(write_bytes);
379                wrote_asset_count += 1;
380            }
381            Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => {
382                prepare_next_frame.assets.push((id, extracted_asset));
383            }
384        }
385    }
386
387    if bpf.exhausted() && !prepare_next_frame.assets.is_empty() {
388        debug!(
389            "{} write budget exhausted with {} assets remaining (wrote {})",
390            std::any::type_name::<A>(),
391            prepare_next_frame.assets.len(),
392            wrote_asset_count
393        );
394    }
395}
396
397/// A resource that attempts to limit the amount of data transferred from cpu to gpu
398/// each frame, preventing choppy frames at the cost of waiting longer for gpu assets
399/// to become available
400#[derive(Resource, Default, Debug, Clone, Copy, ExtractResource)]
401pub struct RenderAssetBytesPerFrame {
402    pub max_bytes: Option<usize>,
403    pub available: usize,
404}
405
406impl RenderAssetBytesPerFrame {
407    /// `max_bytes`: the number of bytes to write per frame.
408    /// this is a soft limit: only full assets are written currently, uploading stops
409    /// after the first asset that exceeds the limit.
410    /// To participate, assets should implement [`RenderAsset::byte_len`]. If the default
411    /// is not overridden, the assets are assumed to be small enough to upload without restriction.
412    pub fn new(max_bytes: usize) -> Self {
413        Self {
414            max_bytes: Some(max_bytes),
415            available: 0,
416        }
417    }
418
419    /// Reset the available bytes. Called once per frame by the [`crate::RenderPlugin`].
420    pub fn reset(&mut self) {
421        self.available = self.max_bytes.unwrap_or(usize::MAX);
422    }
423
424    /// check how many bytes are available since the last reset
425    pub fn available_bytes(&self, required_bytes: usize) -> usize {
426        if self.max_bytes.is_none() {
427            return required_bytes;
428        }
429
430        required_bytes.min(self.available)
431    }
432
433    /// decrease the available bytes for the current frame
434    fn write_bytes(&mut self, bytes: usize) {
435        if self.max_bytes.is_none() {
436            return;
437        }
438
439        let write_bytes = bytes.min(self.available);
440        self.available -= write_bytes;
441    }
442
443    // check if any bytes remain available for writing this frame
444    fn exhausted(&self) -> bool {
445        self.max_bytes.is_some() && self.available == 0
446    }
447}