Path: blob/main/crates/bevy_render/src/erased_render_asset.rs
6595 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, EventReader, IntoScheduleConfigs, ResMut, Resource},9schedule::{ScheduleConfigs, SystemSet},10system::{ScheduleSystem, StaticSystemParam, SystemParam, SystemParamItem, SystemState},11world::{FromWorld, Mut},12};13use bevy_platform::collections::{HashMap, HashSet};14use bevy_render::render_asset::RenderAssetBytesPerFrameLimiter;15use core::marker::PhantomData;16use thiserror::Error;17use tracing::{debug, 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/// Deprecated alias for [`AssetExtractionSystems`].32#[deprecated(since = "0.17.0", note = "Renamed to `AssetExtractionSystems`.")]33pub type ExtractAssetsSet = AssetExtractionSystems;3435/// Describes how an asset gets extracted and prepared for rendering.36///37/// In the [`ExtractSchedule`] step the [`ErasedRenderAsset::SourceAsset`] is transferred38/// from the "main world" into the "render world".39///40/// After that in the [`RenderSystems::PrepareAssets`] step the extracted asset41/// is transformed into its GPU-representation of type [`ErasedRenderAsset`].42pub trait ErasedRenderAsset: Send + Sync + 'static {43/// The representation of the asset in the "main world".44type SourceAsset: Asset + Clone;45/// The target representation of the asset in the "render world".46type ErasedAsset: Send + Sync + 'static + Sized;4748/// Specifies all ECS data required by [`ErasedRenderAsset::prepare_asset`].49///50/// For convenience use the [`lifetimeless`](bevy_ecs::system::lifetimeless) [`SystemParam`].51type Param: SystemParam;5253/// Whether or not to unload the asset after extracting it to the render world.54#[inline]55fn asset_usage(_source_asset: &Self::SourceAsset) -> RenderAssetUsages {56RenderAssetUsages::default()57}5859/// Size of the data the asset will upload to the gpu. Specifying a return value60/// will allow the asset to be throttled via [`RenderAssetBytesPerFrameLimiter`].61#[inline]62#[expect(63unused_variables,64reason = "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."65)]66fn byte_len(erased_asset: &Self::SourceAsset) -> Option<usize> {67None68}6970/// Prepares the [`ErasedRenderAsset::SourceAsset`] for the GPU by transforming it into a [`ErasedRenderAsset`].71///72/// ECS data may be accessed via `param`.73fn prepare_asset(74source_asset: Self::SourceAsset,75asset_id: AssetId<Self::SourceAsset>,76param: &mut SystemParamItem<Self::Param>,77) -> Result<Self::ErasedAsset, PrepareAssetError<Self::SourceAsset>>;7879/// Called whenever the [`ErasedRenderAsset::SourceAsset`] has been removed.80///81/// You can implement this method if you need to access ECS data (via82/// `_param`) in order to perform cleanup tasks when the asset is removed.83///84/// The default implementation does nothing.85fn unload_asset(86_source_asset: AssetId<Self::SourceAsset>,87_param: &mut SystemParamItem<Self::Param>,88) {89}90}9192/// This plugin extracts the changed assets from the "app world" into the "render world"93/// and prepares them for the GPU. They can then be accessed from the [`ErasedRenderAssets`] resource.94///95/// Therefore it sets up the [`ExtractSchedule`] and96/// [`RenderSystems::PrepareAssets`] steps for the specified [`ErasedRenderAsset`].97///98/// The `AFTER` generic parameter can be used to specify that `A::prepare_asset` should not be run until99/// `prepare_assets::<AFTER>` has completed. This allows the `prepare_asset` function to depend on another100/// prepared [`ErasedRenderAsset`], for example `Mesh::prepare_asset` relies on `ErasedRenderAssets::<GpuImage>` for morph101/// targets, so the plugin is created as `ErasedRenderAssetPlugin::<RenderMesh, GpuImage>::default()`.102pub struct ErasedRenderAssetPlugin<103A: ErasedRenderAsset,104AFTER: ErasedRenderAssetDependency + 'static = (),105> {106phantom: PhantomData<fn() -> (A, AFTER)>,107}108109impl<A: ErasedRenderAsset, AFTER: ErasedRenderAssetDependency + 'static> Default110for ErasedRenderAssetPlugin<A, AFTER>111{112fn default() -> Self {113Self {114phantom: Default::default(),115}116}117}118119impl<A: ErasedRenderAsset, AFTER: ErasedRenderAssetDependency + 'static> Plugin120for ErasedRenderAssetPlugin<A, AFTER>121{122fn build(&self, app: &mut App) {123app.init_resource::<CachedExtractErasedRenderAssetSystemState<A>>();124}125126fn finish(&self, app: &mut App) {127if let Some(render_app) = app.get_sub_app_mut(RenderApp) {128render_app129.init_resource::<ExtractedAssets<A>>()130.init_resource::<ErasedRenderAssets<A::ErasedAsset>>()131.init_resource::<PrepareNextFrameAssets<A>>()132.add_systems(133ExtractSchedule,134extract_erased_render_asset::<A>.in_set(AssetExtractionSystems),135);136AFTER::register_system(137render_app,138prepare_erased_assets::<A>.in_set(RenderSystems::PrepareAssets),139);140}141}142}143144// helper to allow specifying dependencies between render assets145pub trait ErasedRenderAssetDependency {146fn register_system(render_app: &mut SubApp, system: ScheduleConfigs<ScheduleSystem>);147}148149impl ErasedRenderAssetDependency for () {150fn register_system(render_app: &mut SubApp, system: ScheduleConfigs<ScheduleSystem>) {151render_app.add_systems(Render, system);152}153}154155impl<A: ErasedRenderAsset> ErasedRenderAssetDependency for A {156fn register_system(render_app: &mut SubApp, system: ScheduleConfigs<ScheduleSystem>) {157render_app.add_systems(Render, system.after(prepare_erased_assets::<A>));158}159}160161/// Temporarily stores the extracted and removed assets of the current frame.162#[derive(Resource)]163pub struct ExtractedAssets<A: ErasedRenderAsset> {164/// The assets extracted this frame.165///166/// These are assets that were either added or modified this frame.167pub extracted: Vec<(AssetId<A::SourceAsset>, A::SourceAsset)>,168169/// IDs of the assets that were removed this frame.170///171/// These assets will not be present in [`ExtractedAssets::extracted`].172pub removed: HashSet<AssetId<A::SourceAsset>>,173174/// IDs of the assets that were modified this frame.175pub modified: HashSet<AssetId<A::SourceAsset>>,176177/// IDs of the assets that were added this frame.178pub added: HashSet<AssetId<A::SourceAsset>>,179}180181impl<A: ErasedRenderAsset> Default for ExtractedAssets<A> {182fn default() -> Self {183Self {184extracted: Default::default(),185removed: Default::default(),186modified: Default::default(),187added: Default::default(),188}189}190}191192/// Stores all GPU representations ([`ErasedRenderAsset`])193/// of [`ErasedRenderAsset::SourceAsset`] as long as they exist.194#[derive(Resource)]195pub struct ErasedRenderAssets<ERA>(HashMap<UntypedAssetId, ERA>);196197impl<ERA> Default for ErasedRenderAssets<ERA> {198fn default() -> Self {199Self(Default::default())200}201}202203impl<ERA> ErasedRenderAssets<ERA> {204pub fn get(&self, id: impl Into<UntypedAssetId>) -> Option<&ERA> {205self.0.get(&id.into())206}207208pub fn get_mut(&mut self, id: impl Into<UntypedAssetId>) -> Option<&mut ERA> {209self.0.get_mut(&id.into())210}211212pub fn insert(&mut self, id: impl Into<UntypedAssetId>, value: ERA) -> Option<ERA> {213self.0.insert(id.into(), value)214}215216pub fn remove(&mut self, id: impl Into<UntypedAssetId>) -> Option<ERA> {217self.0.remove(&id.into())218}219220pub fn iter(&self) -> impl Iterator<Item = (UntypedAssetId, &ERA)> {221self.0.iter().map(|(k, v)| (*k, v))222}223224pub fn iter_mut(&mut self) -> impl Iterator<Item = (UntypedAssetId, &mut ERA)> {225self.0.iter_mut().map(|(k, v)| (*k, v))226}227}228229#[derive(Resource)]230struct CachedExtractErasedRenderAssetSystemState<A: ErasedRenderAsset> {231state: SystemState<(232EventReader<'static, 'static, AssetEvent<A::SourceAsset>>,233ResMut<'static, Assets<A::SourceAsset>>,234)>,235}236237impl<A: ErasedRenderAsset> FromWorld for CachedExtractErasedRenderAssetSystemState<A> {238fn from_world(world: &mut bevy_ecs::world::World) -> Self {239Self {240state: SystemState::new(world),241}242}243}244245/// This system extracts all created or modified assets of the corresponding [`ErasedRenderAsset::SourceAsset`] type246/// into the "render world".247pub(crate) fn extract_erased_render_asset<A: ErasedRenderAsset>(248mut commands: Commands,249mut main_world: ResMut<MainWorld>,250) {251main_world.resource_scope(252|world, mut cached_state: Mut<CachedExtractErasedRenderAssetSystemState<A>>| {253let (mut events, mut assets) = cached_state.state.get_mut(world);254255let mut needs_extracting = <HashSet<_>>::default();256let mut removed = <HashSet<_>>::default();257let mut modified = <HashSet<_>>::default();258259for event in events.read() {260#[expect(261clippy::match_same_arms,262reason = "LoadedWithDependencies is marked as a TODO, so it's likely this will no longer lint soon."263)]264match event {265AssetEvent::Added { id } => {266needs_extracting.insert(*id);267}268AssetEvent::Modified { id } => {269needs_extracting.insert(*id);270modified.insert(*id);271}272AssetEvent::Removed { .. } => {273// We don't care that the asset was removed from Assets<T> in the main world.274// An asset is only removed from ErasedRenderAssets<T> when its last handle is dropped (AssetEvent::Unused).275}276AssetEvent::Unused { id } => {277needs_extracting.remove(id);278modified.remove(id);279removed.insert(*id);280}281AssetEvent::LoadedWithDependencies { .. } => {282// TODO: handle this283}284}285}286287let mut extracted_assets = Vec::new();288let mut added = <HashSet<_>>::default();289for id in needs_extracting.drain() {290if let Some(asset) = assets.get(id) {291let asset_usage = A::asset_usage(asset);292if asset_usage.contains(RenderAssetUsages::RENDER_WORLD) {293if asset_usage == RenderAssetUsages::RENDER_WORLD {294if let Some(asset) = assets.remove(id) {295extracted_assets.push((id, asset));296added.insert(id);297}298} else {299extracted_assets.push((id, asset.clone()));300added.insert(id);301}302}303}304}305306commands.insert_resource(ExtractedAssets::<A> {307extracted: extracted_assets,308removed,309modified,310added,311});312cached_state.state.apply(world);313},314);315}316317// TODO: consider storing inside system?318/// All assets that should be prepared next frame.319#[derive(Resource)]320pub struct PrepareNextFrameAssets<A: ErasedRenderAsset> {321assets: Vec<(AssetId<A::SourceAsset>, A::SourceAsset)>,322}323324impl<A: ErasedRenderAsset> Default for PrepareNextFrameAssets<A> {325fn default() -> Self {326Self {327assets: Default::default(),328}329}330}331332/// This system prepares all assets of the corresponding [`ErasedRenderAsset::SourceAsset`] type333/// which where extracted this frame for the GPU.334pub fn prepare_erased_assets<A: ErasedRenderAsset>(335mut extracted_assets: ResMut<ExtractedAssets<A>>,336mut render_assets: ResMut<ErasedRenderAssets<A::ErasedAsset>>,337mut prepare_next_frame: ResMut<PrepareNextFrameAssets<A>>,338param: StaticSystemParam<<A as ErasedRenderAsset>::Param>,339bpf: Res<RenderAssetBytesPerFrameLimiter>,340) {341let mut wrote_asset_count = 0;342343let mut param = param.into_inner();344let queued_assets = core::mem::take(&mut prepare_next_frame.assets);345for (id, extracted_asset) in queued_assets {346if extracted_assets.removed.contains(&id) || extracted_assets.added.contains(&id) {347// skip previous frame's assets that have been removed or updated348continue;349}350351let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) {352// we could check if available bytes > byte_len here, but we want to make some353// forward progress even if the asset is larger than the max bytes per frame.354// this way we always write at least one (sized) asset per frame.355// in future we could also consider partial asset uploads.356if bpf.exhausted() {357prepare_next_frame.assets.push((id, extracted_asset));358continue;359}360size361} else {3620363};364365match A::prepare_asset(extracted_asset, id, &mut param) {366Ok(prepared_asset) => {367render_assets.insert(id, prepared_asset);368bpf.write_bytes(write_bytes);369wrote_asset_count += 1;370}371Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => {372prepare_next_frame.assets.push((id, extracted_asset));373}374Err(PrepareAssetError::AsBindGroupError(e)) => {375error!(376"{} Bind group construction failed: {e}",377core::any::type_name::<A>()378);379}380}381}382383for removed in extracted_assets.removed.drain() {384render_assets.remove(removed);385A::unload_asset(removed, &mut param);386}387388for (id, extracted_asset) in extracted_assets.extracted.drain(..) {389// we remove previous here to ensure that if we are updating the asset then390// any users will not see the old asset after a new asset is extracted,391// even if the new asset is not yet ready or we are out of bytes to write.392render_assets.remove(id);393394let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) {395if bpf.exhausted() {396prepare_next_frame.assets.push((id, extracted_asset));397continue;398}399size400} else {4010402};403404match A::prepare_asset(extracted_asset, id, &mut param) {405Ok(prepared_asset) => {406render_assets.insert(id, prepared_asset);407bpf.write_bytes(write_bytes);408wrote_asset_count += 1;409}410Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => {411prepare_next_frame.assets.push((id, extracted_asset));412}413Err(PrepareAssetError::AsBindGroupError(e)) => {414error!(415"{} Bind group construction failed: {e}",416core::any::type_name::<A>()417);418}419}420}421422if bpf.exhausted() && !prepare_next_frame.assets.is_empty() {423debug!(424"{} write budget exhausted with {} assets remaining (wrote {})",425core::any::type_name::<A>(),426prepare_next_frame.assets.len(),427wrote_asset_count428);429}430}431432433