bevy_render/
render_asset.rs

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