Path: blob/main/crates/bevy_render/src/erased_render_asset.rs
9395 views
use crate::{1render_resource::AsBindGroupError, ExtractSchedule, MainWorld, Render, RenderApp,2RenderSystems, Res,3};4use bevy_app::{App, Plugin, SubApp};5use bevy_asset::RenderAssetUsages;6use bevy_asset::{Asset, AssetEvent, AssetId, Assets, UntypedAssetId};7use bevy_ecs::{8prelude::{Commands, IntoScheduleConfigs, MessageReader, ResMut, Resource},9schedule::{ScheduleConfigs, SystemSet},10system::{ScheduleSystem, StaticSystemParam, SystemParam, SystemParamItem, SystemState},11world::{FromWorld, Mut},12};13use bevy_log::{debug, error};14use bevy_platform::collections::{HashMap, HashSet};15use bevy_render::render_asset::RenderAssetBytesPerFrameLimiter;16use core::marker::PhantomData;17use thiserror::Error;1819#[derive(Debug, Error)]20pub enum PrepareAssetError<E: Send + Sync + 'static> {21#[error("Failed to prepare asset")]22RetryNextUpdate(E),23#[error("Failed to build bind group: {0}")]24AsBindGroupError(AsBindGroupError),25}2627/// The system set during which we extract modified assets to the render world.28#[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)]29pub struct AssetExtractionSystems;3031/// Describes how an asset gets extracted and prepared for rendering.32///33/// In the [`ExtractSchedule`] step the [`ErasedRenderAsset::SourceAsset`] is transferred34/// from the "main world" into the "render world".35///36/// After that in the [`RenderSystems::PrepareAssets`] step the extracted asset37/// is transformed into its GPU-representation of type [`ErasedRenderAsset`].38pub trait ErasedRenderAsset: Send + Sync + 'static {39/// The representation of the asset in the "main world".40type SourceAsset: Asset + Clone;41/// The target representation of the asset in the "render world".42type ErasedAsset: Send + Sync + 'static + Sized;4344/// Specifies all ECS data required by [`ErasedRenderAsset::prepare_asset`].45///46/// For convenience use the [`lifetimeless`](bevy_ecs::system::lifetimeless) [`SystemParam`].47type Param: SystemParam;4849/// Whether or not to unload the asset after extracting it to the render world.50#[inline]51fn asset_usage(_source_asset: &Self::SourceAsset) -> RenderAssetUsages {52RenderAssetUsages::default()53}5455/// Size of the data the asset will upload to the gpu. Specifying a return value56/// will allow the asset to be throttled via [`RenderAssetBytesPerFrameLimiter`].57#[inline]58#[expect(59unused_variables,60reason = "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."61)]62fn byte_len(erased_asset: &Self::SourceAsset) -> Option<usize> {63None64}6566/// Prepares the [`ErasedRenderAsset::SourceAsset`] for the GPU by transforming it into a [`ErasedRenderAsset`].67///68/// ECS data may be accessed via `param`.69fn prepare_asset(70source_asset: Self::SourceAsset,71asset_id: AssetId<Self::SourceAsset>,72param: &mut SystemParamItem<Self::Param>,73) -> Result<Self::ErasedAsset, PrepareAssetError<Self::SourceAsset>>;7475/// Called whenever the [`ErasedRenderAsset::SourceAsset`] has been removed.76///77/// You can implement this method if you need to access ECS data (via78/// `_param`) in order to perform cleanup tasks when the asset is removed.79///80/// The default implementation does nothing.81fn unload_asset(82_source_asset: AssetId<Self::SourceAsset>,83_param: &mut SystemParamItem<Self::Param>,84) {85}86}8788/// This plugin extracts the changed assets from the "app world" into the "render world"89/// and prepares them for the GPU. They can then be accessed from the [`ErasedRenderAssets`] resource.90///91/// Therefore it sets up the [`ExtractSchedule`] and92/// [`RenderSystems::PrepareAssets`] steps for the specified [`ErasedRenderAsset`].93///94/// The `AFTER` generic parameter can be used to specify that `A::prepare_asset` should not be run until95/// `prepare_assets::<AFTER>` has completed. This allows the `prepare_asset` function to depend on another96/// prepared [`ErasedRenderAsset`], for example `Mesh::prepare_asset` relies on `ErasedRenderAssets::<GpuImage>` for morph97/// targets, so the plugin is created as `ErasedRenderAssetPlugin::<RenderMesh, GpuImage>::default()`.98pub struct ErasedRenderAssetPlugin<99A: ErasedRenderAsset,100AFTER: ErasedRenderAssetDependency + 'static = (),101> {102phantom: PhantomData<fn() -> (A, AFTER)>,103}104105impl<A: ErasedRenderAsset, AFTER: ErasedRenderAssetDependency + 'static> Default106for ErasedRenderAssetPlugin<A, AFTER>107{108fn default() -> Self {109Self {110phantom: Default::default(),111}112}113}114115impl<A: ErasedRenderAsset, AFTER: ErasedRenderAssetDependency + 'static> Plugin116for ErasedRenderAssetPlugin<A, AFTER>117{118fn build(&self, app: &mut App) {119app.init_resource::<CachedExtractErasedRenderAssetSystemState<A>>();120}121122fn finish(&self, app: &mut App) {123if let Some(render_app) = app.get_sub_app_mut(RenderApp) {124render_app125.init_resource::<ExtractedAssets<A>>()126.init_resource::<ErasedRenderAssets<A::ErasedAsset>>()127.init_resource::<PrepareNextFrameAssets<A>>()128.add_systems(129ExtractSchedule,130extract_erased_render_asset::<A>.in_set(AssetExtractionSystems),131);132AFTER::register_system(133render_app,134prepare_erased_assets::<A>.in_set(RenderSystems::PrepareAssets),135);136}137}138}139140// helper to allow specifying dependencies between render assets141pub trait ErasedRenderAssetDependency {142fn register_system(render_app: &mut SubApp, system: ScheduleConfigs<ScheduleSystem>);143}144145impl ErasedRenderAssetDependency for () {146fn register_system(render_app: &mut SubApp, system: ScheduleConfigs<ScheduleSystem>) {147render_app.add_systems(Render, system);148}149}150151impl<A: ErasedRenderAsset> ErasedRenderAssetDependency for A {152fn register_system(render_app: &mut SubApp, system: ScheduleConfigs<ScheduleSystem>) {153render_app.add_systems(Render, system.after(prepare_erased_assets::<A>));154}155}156157/// Temporarily stores the extracted and removed assets of the current frame.158#[derive(Resource)]159pub struct ExtractedAssets<A: ErasedRenderAsset> {160/// The assets extracted this frame.161///162/// These are assets that were either added or modified this frame.163pub extracted: Vec<(AssetId<A::SourceAsset>, A::SourceAsset)>,164165/// IDs of the assets that were removed this frame.166///167/// These assets will not be present in [`ExtractedAssets::extracted`].168pub removed: HashSet<AssetId<A::SourceAsset>>,169170/// IDs of the assets that were modified this frame.171pub modified: HashSet<AssetId<A::SourceAsset>>,172173/// IDs of the assets that were added this frame.174pub added: HashSet<AssetId<A::SourceAsset>>,175}176177impl<A: ErasedRenderAsset> Default for ExtractedAssets<A> {178fn default() -> Self {179Self {180extracted: Default::default(),181removed: Default::default(),182modified: Default::default(),183added: Default::default(),184}185}186}187188/// Stores all GPU representations ([`ErasedRenderAsset`])189/// of [`ErasedRenderAsset::SourceAsset`] as long as they exist.190#[derive(Resource)]191pub struct ErasedRenderAssets<ERA>(HashMap<UntypedAssetId, ERA>);192193impl<ERA> Default for ErasedRenderAssets<ERA> {194fn default() -> Self {195Self(Default::default())196}197}198199impl<ERA> ErasedRenderAssets<ERA> {200pub fn get(&self, id: impl Into<UntypedAssetId>) -> Option<&ERA> {201self.0.get(&id.into())202}203204pub fn get_mut(&mut self, id: impl Into<UntypedAssetId>) -> Option<&mut ERA> {205self.0.get_mut(&id.into())206}207208pub fn insert(&mut self, id: impl Into<UntypedAssetId>, value: ERA) -> Option<ERA> {209self.0.insert(id.into(), value)210}211212pub fn remove(&mut self, id: impl Into<UntypedAssetId>) -> Option<ERA> {213self.0.remove(&id.into())214}215216pub fn iter(&self) -> impl Iterator<Item = (UntypedAssetId, &ERA)> {217self.0.iter().map(|(k, v)| (*k, v))218}219220pub fn iter_mut(&mut self) -> impl Iterator<Item = (UntypedAssetId, &mut ERA)> {221self.0.iter_mut().map(|(k, v)| (*k, v))222}223}224225#[derive(Resource)]226struct CachedExtractErasedRenderAssetSystemState<A: ErasedRenderAsset> {227state: SystemState<(228MessageReader<'static, 'static, AssetEvent<A::SourceAsset>>,229ResMut<'static, Assets<A::SourceAsset>>,230)>,231}232233impl<A: ErasedRenderAsset> FromWorld for CachedExtractErasedRenderAssetSystemState<A> {234fn from_world(world: &mut bevy_ecs::world::World) -> Self {235Self {236state: SystemState::new(world),237}238}239}240241/// This system extracts all created or modified assets of the corresponding [`ErasedRenderAsset::SourceAsset`] type242/// into the "render world".243pub(crate) fn extract_erased_render_asset<A: ErasedRenderAsset>(244mut commands: Commands,245mut main_world: ResMut<MainWorld>,246) {247main_world.resource_scope(248|world, mut cached_state: Mut<CachedExtractErasedRenderAssetSystemState<A>>| {249let (mut events, mut assets) = cached_state.state.get_mut(world);250251let mut needs_extracting = <HashSet<_>>::default();252let mut removed = <HashSet<_>>::default();253let mut modified = <HashSet<_>>::default();254255for event in events.read() {256#[expect(257clippy::match_same_arms,258reason = "LoadedWithDependencies is marked as a TODO, so it's likely this will no longer lint soon."259)]260match event {261AssetEvent::Added { id } => {262needs_extracting.insert(*id);263}264AssetEvent::Modified { id } => {265needs_extracting.insert(*id);266modified.insert(*id);267}268AssetEvent::Removed { .. } => {269// We don't care that the asset was removed from Assets<T> in the main world.270// An asset is only removed from ErasedRenderAssets<T> when its last handle is dropped (AssetEvent::Unused).271}272AssetEvent::Unused { id } => {273needs_extracting.remove(id);274modified.remove(id);275removed.insert(*id);276}277AssetEvent::LoadedWithDependencies { .. } => {278// TODO: handle this279}280}281}282283let mut extracted_assets = Vec::new();284let mut added = <HashSet<_>>::default();285for id in needs_extracting.drain() {286if let Some(asset) = assets.get(id) {287let asset_usage = A::asset_usage(asset);288if asset_usage.contains(RenderAssetUsages::RENDER_WORLD) {289if asset_usage == RenderAssetUsages::RENDER_WORLD {290if let Some(asset) = assets.remove(id) {291extracted_assets.push((id, asset));292added.insert(id);293}294} else {295extracted_assets.push((id, asset.clone()));296added.insert(id);297}298}299}300}301302commands.insert_resource(ExtractedAssets::<A> {303extracted: extracted_assets,304removed,305modified,306added,307});308cached_state.state.apply(world);309},310);311}312313// TODO: consider storing inside system?314/// All assets that should be prepared next frame.315#[derive(Resource)]316pub struct PrepareNextFrameAssets<A: ErasedRenderAsset> {317assets: Vec<(AssetId<A::SourceAsset>, A::SourceAsset)>,318}319320impl<A: ErasedRenderAsset> Default for PrepareNextFrameAssets<A> {321fn default() -> Self {322Self {323assets: Default::default(),324}325}326}327328/// This system prepares all assets of the corresponding [`ErasedRenderAsset::SourceAsset`] type329/// which where extracted this frame for the GPU.330pub fn prepare_erased_assets<A: ErasedRenderAsset>(331mut extracted_assets: ResMut<ExtractedAssets<A>>,332mut render_assets: ResMut<ErasedRenderAssets<A::ErasedAsset>>,333mut prepare_next_frame: ResMut<PrepareNextFrameAssets<A>>,334param: StaticSystemParam<<A as ErasedRenderAsset>::Param>,335bpf: Res<RenderAssetBytesPerFrameLimiter>,336) {337let mut wrote_asset_count = 0;338339let mut param = param.into_inner();340let queued_assets = core::mem::take(&mut prepare_next_frame.assets);341for (id, extracted_asset) in queued_assets {342if extracted_assets.removed.contains(&id) || extracted_assets.added.contains(&id) {343// skip previous frame's assets that have been removed or updated344continue;345}346347let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) {348// we could check if available bytes > byte_len here, but we want to make some349// forward progress even if the asset is larger than the max bytes per frame.350// this way we always write at least one (sized) asset per frame.351// in future we could also consider partial asset uploads.352if bpf.exhausted() {353prepare_next_frame.assets.push((id, extracted_asset));354continue;355}356size357} else {3580359};360361match A::prepare_asset(extracted_asset, id, &mut param) {362Ok(prepared_asset) => {363render_assets.insert(id, prepared_asset);364bpf.write_bytes(write_bytes);365wrote_asset_count += 1;366}367Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => {368prepare_next_frame.assets.push((id, extracted_asset));369}370Err(PrepareAssetError::AsBindGroupError(e)) => {371error!(372"{} Bind group construction failed: {e}",373core::any::type_name::<A>()374);375}376}377}378379for removed in extracted_assets.removed.drain() {380render_assets.remove(removed);381A::unload_asset(removed, &mut param);382}383384for (id, extracted_asset) in extracted_assets.extracted.drain(..) {385// we remove previous here to ensure that if we are updating the asset then386// any users will not see the old asset after a new asset is extracted,387// even if the new asset is not yet ready or we are out of bytes to write.388render_assets.remove(id);389390let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) {391if bpf.exhausted() {392prepare_next_frame.assets.push((id, extracted_asset));393continue;394}395size396} else {3970398};399400match A::prepare_asset(extracted_asset, id, &mut param) {401Ok(prepared_asset) => {402render_assets.insert(id, prepared_asset);403bpf.write_bytes(write_bytes);404wrote_asset_count += 1;405}406Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => {407prepare_next_frame.assets.push((id, extracted_asset));408}409Err(PrepareAssetError::AsBindGroupError(e)) => {410error!(411"{} Bind group construction failed: {e}",412core::any::type_name::<A>()413);414}415}416}417418if bpf.exhausted() && !prepare_next_frame.assets.is_empty() {419debug!(420"{} write budget exhausted with {} assets remaining (wrote {})",421core::any::type_name::<A>(),422prepare_next_frame.assets.len(),423wrote_asset_count424);425}426}427428429