bevy_render/
erased_render_asset.rs

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