use crate::{ render_resource::AsBindGroupError, ExtractSchedule, MainWorld, Render, RenderApp, RenderSystems, Res, }; use bevy_app::{App, Plugin, SubApp}; pub use bevy_asset::RenderAssetUsages; use bevy_asset::{Asset, AssetEvent, AssetId, Assets, UntypedAssetId}; use bevy_ecs::{ prelude::{Commands, EventReader, IntoScheduleConfigs, ResMut, Resource}, schedule::{ScheduleConfigs, SystemSet}, system::{ScheduleSystem, StaticSystemParam, SystemParam, SystemParamItem, SystemState}, world::{FromWorld, Mut}, }; use bevy_platform::collections::{HashMap, HashSet}; use bevy_render::render_asset::RenderAssetBytesPerFrameLimiter; use core::marker::PhantomData; use thiserror::Error; use tracing::{debug, error}; #[derive(Debug, Error)] pub enum PrepareAssetError { #[error("Failed to prepare asset")] RetryNextUpdate(E), #[error("Failed to build bind group: {0}")] AsBindGroupError(AsBindGroupError), } /// The system set during which we extract modified assets to the render world. #[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)] pub struct AssetExtractionSystems; /// Deprecated alias for [`AssetExtractionSystems`]. #[deprecated(since = "0.17.0", note = "Renamed to `AssetExtractionSystems`.")] pub type ExtractAssetsSet = AssetExtractionSystems; /// Describes how an asset gets extracted and prepared for rendering. /// /// In the [`ExtractSchedule`] step the [`ErasedRenderAsset::SourceAsset`] is transferred /// from the "main world" into the "render world". /// /// After that in the [`RenderSystems::PrepareAssets`] step the extracted asset /// is transformed into its GPU-representation of type [`ErasedRenderAsset`]. pub trait ErasedRenderAsset: Send + Sync + 'static { /// The representation of the asset in the "main world". type SourceAsset: Asset + Clone; /// The target representation of the asset in the "render world". type ErasedAsset: Send + Sync + 'static + Sized; /// Specifies all ECS data required by [`ErasedRenderAsset::prepare_asset`]. /// /// For convenience use the [`lifetimeless`](bevy_ecs::system::lifetimeless) [`SystemParam`]. type Param: SystemParam; /// Whether or not to unload the asset after extracting it to the render world. #[inline] fn asset_usage(_source_asset: &Self::SourceAsset) -> RenderAssetUsages { RenderAssetUsages::default() } /// Size of the data the asset will upload to the gpu. Specifying a return value /// will allow the asset to be throttled via [`RenderAssetBytesPerFrameLimiter`]. #[inline] #[expect( unused_variables, 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." )] fn byte_len(erased_asset: &Self::SourceAsset) -> Option { None } /// Prepares the [`ErasedRenderAsset::SourceAsset`] for the GPU by transforming it into a [`ErasedRenderAsset`]. /// /// ECS data may be accessed via `param`. fn prepare_asset( source_asset: Self::SourceAsset, asset_id: AssetId, param: &mut SystemParamItem, ) -> Result>; /// Called whenever the [`ErasedRenderAsset::SourceAsset`] has been removed. /// /// You can implement this method if you need to access ECS data (via /// `_param`) in order to perform cleanup tasks when the asset is removed. /// /// The default implementation does nothing. fn unload_asset( _source_asset: AssetId, _param: &mut SystemParamItem, ) { } } /// This plugin extracts the changed assets from the "app world" into the "render world" /// and prepares them for the GPU. They can then be accessed from the [`ErasedRenderAssets`] resource. /// /// Therefore it sets up the [`ExtractSchedule`] and /// [`RenderSystems::PrepareAssets`] steps for the specified [`ErasedRenderAsset`]. /// /// The `AFTER` generic parameter can be used to specify that `A::prepare_asset` should not be run until /// `prepare_assets::` has completed. This allows the `prepare_asset` function to depend on another /// prepared [`ErasedRenderAsset`], for example `Mesh::prepare_asset` relies on `ErasedRenderAssets::` for morph /// targets, so the plugin is created as `ErasedRenderAssetPlugin::::default()`. pub struct ErasedRenderAssetPlugin< A: ErasedRenderAsset, AFTER: ErasedRenderAssetDependency + 'static = (), > { phantom: PhantomData (A, AFTER)>, } impl Default for ErasedRenderAssetPlugin { fn default() -> Self { Self { phantom: Default::default(), } } } impl Plugin for ErasedRenderAssetPlugin { fn build(&self, app: &mut App) { app.init_resource::>(); } fn finish(&self, app: &mut App) { if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app .init_resource::>() .init_resource::>() .init_resource::>() .add_systems( ExtractSchedule, extract_erased_render_asset::.in_set(AssetExtractionSystems), ); AFTER::register_system( render_app, prepare_erased_assets::.in_set(RenderSystems::PrepareAssets), ); } } } // helper to allow specifying dependencies between render assets pub trait ErasedRenderAssetDependency { fn register_system(render_app: &mut SubApp, system: ScheduleConfigs); } impl ErasedRenderAssetDependency for () { fn register_system(render_app: &mut SubApp, system: ScheduleConfigs) { render_app.add_systems(Render, system); } } impl ErasedRenderAssetDependency for A { fn register_system(render_app: &mut SubApp, system: ScheduleConfigs) { render_app.add_systems(Render, system.after(prepare_erased_assets::)); } } /// Temporarily stores the extracted and removed assets of the current frame. #[derive(Resource)] pub struct ExtractedAssets { /// The assets extracted this frame. /// /// These are assets that were either added or modified this frame. pub extracted: Vec<(AssetId, A::SourceAsset)>, /// IDs of the assets that were removed this frame. /// /// These assets will not be present in [`ExtractedAssets::extracted`]. pub removed: HashSet>, /// IDs of the assets that were modified this frame. pub modified: HashSet>, /// IDs of the assets that were added this frame. pub added: HashSet>, } impl Default for ExtractedAssets { fn default() -> Self { Self { extracted: Default::default(), removed: Default::default(), modified: Default::default(), added: Default::default(), } } } /// Stores all GPU representations ([`ErasedRenderAsset`]) /// of [`ErasedRenderAsset::SourceAsset`] as long as they exist. #[derive(Resource)] pub struct ErasedRenderAssets(HashMap); impl Default for ErasedRenderAssets { fn default() -> Self { Self(Default::default()) } } impl ErasedRenderAssets { pub fn get(&self, id: impl Into) -> Option<&ERA> { self.0.get(&id.into()) } pub fn get_mut(&mut self, id: impl Into) -> Option<&mut ERA> { self.0.get_mut(&id.into()) } pub fn insert(&mut self, id: impl Into, value: ERA) -> Option { self.0.insert(id.into(), value) } pub fn remove(&mut self, id: impl Into) -> Option { self.0.remove(&id.into()) } pub fn iter(&self) -> impl Iterator { self.0.iter().map(|(k, v)| (*k, v)) } pub fn iter_mut(&mut self) -> impl Iterator { self.0.iter_mut().map(|(k, v)| (*k, v)) } } #[derive(Resource)] struct CachedExtractErasedRenderAssetSystemState { state: SystemState<( EventReader<'static, 'static, AssetEvent>, ResMut<'static, Assets>, )>, } impl FromWorld for CachedExtractErasedRenderAssetSystemState { fn from_world(world: &mut bevy_ecs::world::World) -> Self { Self { state: SystemState::new(world), } } } /// This system extracts all created or modified assets of the corresponding [`ErasedRenderAsset::SourceAsset`] type /// into the "render world". pub(crate) fn extract_erased_render_asset( mut commands: Commands, mut main_world: ResMut, ) { main_world.resource_scope( |world, mut cached_state: Mut>| { let (mut events, mut assets) = cached_state.state.get_mut(world); let mut needs_extracting = >::default(); let mut removed = >::default(); let mut modified = >::default(); for event in events.read() { #[expect( clippy::match_same_arms, reason = "LoadedWithDependencies is marked as a TODO, so it's likely this will no longer lint soon." )] match event { AssetEvent::Added { id } => { needs_extracting.insert(*id); } AssetEvent::Modified { id } => { needs_extracting.insert(*id); modified.insert(*id); } AssetEvent::Removed { .. } => { // We don't care that the asset was removed from Assets in the main world. // An asset is only removed from ErasedRenderAssets when its last handle is dropped (AssetEvent::Unused). } AssetEvent::Unused { id } => { needs_extracting.remove(id); modified.remove(id); removed.insert(*id); } AssetEvent::LoadedWithDependencies { .. } => { // TODO: handle this } } } let mut extracted_assets = Vec::new(); let mut added = >::default(); for id in needs_extracting.drain() { if let Some(asset) = assets.get(id) { let asset_usage = A::asset_usage(asset); if asset_usage.contains(RenderAssetUsages::RENDER_WORLD) { if asset_usage == RenderAssetUsages::RENDER_WORLD { if let Some(asset) = assets.remove(id) { extracted_assets.push((id, asset)); added.insert(id); } } else { extracted_assets.push((id, asset.clone())); added.insert(id); } } } } commands.insert_resource(ExtractedAssets:: { extracted: extracted_assets, removed, modified, added, }); cached_state.state.apply(world); }, ); } // TODO: consider storing inside system? /// All assets that should be prepared next frame. #[derive(Resource)] pub struct PrepareNextFrameAssets { assets: Vec<(AssetId, A::SourceAsset)>, } impl Default for PrepareNextFrameAssets { fn default() -> Self { Self { assets: Default::default(), } } } /// This system prepares all assets of the corresponding [`ErasedRenderAsset::SourceAsset`] type /// which where extracted this frame for the GPU. pub fn prepare_erased_assets( mut extracted_assets: ResMut>, mut render_assets: ResMut>, mut prepare_next_frame: ResMut>, param: StaticSystemParam<::Param>, bpf: Res, ) { let mut wrote_asset_count = 0; let mut param = param.into_inner(); let queued_assets = core::mem::take(&mut prepare_next_frame.assets); for (id, extracted_asset) in queued_assets { if extracted_assets.removed.contains(&id) || extracted_assets.added.contains(&id) { // skip previous frame's assets that have been removed or updated continue; } let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) { // we could check if available bytes > byte_len here, but we want to make some // forward progress even if the asset is larger than the max bytes per frame. // this way we always write at least one (sized) asset per frame. // in future we could also consider partial asset uploads. if bpf.exhausted() { prepare_next_frame.assets.push((id, extracted_asset)); continue; } size } else { 0 }; match A::prepare_asset(extracted_asset, id, &mut param) { Ok(prepared_asset) => { render_assets.insert(id, prepared_asset); bpf.write_bytes(write_bytes); wrote_asset_count += 1; } Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => { prepare_next_frame.assets.push((id, extracted_asset)); } Err(PrepareAssetError::AsBindGroupError(e)) => { error!( "{} Bind group construction failed: {e}", core::any::type_name::() ); } } } for removed in extracted_assets.removed.drain() { render_assets.remove(removed); A::unload_asset(removed, &mut param); } for (id, extracted_asset) in extracted_assets.extracted.drain(..) { // we remove previous here to ensure that if we are updating the asset then // any users will not see the old asset after a new asset is extracted, // even if the new asset is not yet ready or we are out of bytes to write. render_assets.remove(id); let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) { if bpf.exhausted() { prepare_next_frame.assets.push((id, extracted_asset)); continue; } size } else { 0 }; match A::prepare_asset(extracted_asset, id, &mut param) { Ok(prepared_asset) => { render_assets.insert(id, prepared_asset); bpf.write_bytes(write_bytes); wrote_asset_count += 1; } Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => { prepare_next_frame.assets.push((id, extracted_asset)); } Err(PrepareAssetError::AsBindGroupError(e)) => { error!( "{} Bind group construction failed: {e}", core::any::type_name::() ); } } } if bpf.exhausted() && !prepare_next_frame.assets.is_empty() { debug!( "{} write budget exhausted with {} assets remaining (wrote {})", core::any::type_name::(), prepare_next_frame.assets.len(), wrote_asset_count ); } }