1use crate::{
2 render_resource::AsBindGroupError, ExtractSchedule, MainWorld, Render, RenderApp,
3 RenderStartup, 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, Local, MessageReader, ResMut, Resource},
10 schedule::{ScheduleConfigs, SystemSet},
11 system::{ScheduleSystem, StaticSystemParam, SystemParam, SystemParamItem, SystemState},
12 world::{FromWorld, Mut},
13};
14use bevy_log::{debug, error};
15use bevy_platform::collections::{HashMap, HashSet};
16use bevy_render::render_asset::RenderAssetBytesPerFrameLimiter;
17use core::marker::PhantomData;
18use thiserror::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#[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)]
30pub struct AssetExtractionSystems;
31
32pub trait ErasedRenderAsset: Send + Sync + 'static {
40 type SourceAsset: Asset + Clone;
42 type ErasedAsset: Send + Sync + 'static + Sized;
44
45 type Param: SystemParam;
49
50 #[inline]
52 fn asset_usage(_source_asset: &Self::SourceAsset) -> RenderAssetUsages {
53 RenderAssetUsages::default()
54 }
55
56 #[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 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 fn unload_asset(
83 _source_asset: AssetId<Self::SourceAsset>,
84 _param: &mut SystemParamItem<Self::Param>,
85 ) {
86 }
87}
88
89pub 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 .allow_ambiguous_resource::<ErasedRenderAssets<A::ErasedAsset>>()
129 .init_resource::<PrepareNextFrameAssets<A>>()
130 .add_systems(
131 RenderStartup,
132 collect_erased_render_assets_to_reextract::<A>,
133 )
134 .add_systems(
135 ExtractSchedule,
136 extract_erased_render_asset::<A>.in_set(AssetExtractionSystems),
137 );
138 AFTER::register_system(
139 render_app,
140 prepare_erased_assets::<A>.in_set(RenderSystems::PrepareAssets),
141 );
142 }
143 }
144}
145
146pub trait ErasedRenderAssetDependency {
148 fn register_system(render_app: &mut SubApp, system: ScheduleConfigs<ScheduleSystem>);
149}
150
151impl ErasedRenderAssetDependency for () {
152 fn register_system(render_app: &mut SubApp, system: ScheduleConfigs<ScheduleSystem>) {
153 render_app.add_systems(Render, system);
154 }
155}
156
157impl<A: ErasedRenderAsset> ErasedRenderAssetDependency for A {
158 fn register_system(render_app: &mut SubApp, system: ScheduleConfigs<ScheduleSystem>) {
159 render_app.add_systems(Render, system.after(prepare_erased_assets::<A>));
160 }
161}
162
163#[derive(Resource)]
165pub struct ExtractedAssets<A: ErasedRenderAsset> {
166 pub extracted: Vec<(AssetId<A::SourceAsset>, A::SourceAsset)>,
170
171 pub removed: HashSet<AssetId<A::SourceAsset>>,
175
176 pub modified: HashSet<AssetId<A::SourceAsset>>,
178
179 pub added: HashSet<AssetId<A::SourceAsset>>,
181}
182
183impl<A: ErasedRenderAsset> Default for ExtractedAssets<A> {
184 fn default() -> Self {
185 Self {
186 extracted: Default::default(),
187 removed: Default::default(),
188 modified: Default::default(),
189 added: Default::default(),
190 }
191 }
192}
193
194#[derive(Resource)]
197pub struct ErasedRenderAssets<ERA>(HashMap<UntypedAssetId, ERA>);
198
199impl<ERA> Default for ErasedRenderAssets<ERA> {
200 fn default() -> Self {
201 Self(Default::default())
202 }
203}
204
205impl<ERA> ErasedRenderAssets<ERA> {
206 pub fn get(&self, id: impl Into<UntypedAssetId>) -> Option<&ERA> {
207 self.0.get(&id.into())
208 }
209
210 pub fn get_mut(&mut self, id: impl Into<UntypedAssetId>) -> Option<&mut ERA> {
211 self.0.get_mut(&id.into())
212 }
213
214 pub fn insert(&mut self, id: impl Into<UntypedAssetId>, value: ERA) -> Option<ERA> {
215 self.0.insert(id.into(), value)
216 }
217
218 pub fn remove(&mut self, id: impl Into<UntypedAssetId>) -> Option<ERA> {
219 self.0.remove(&id.into())
220 }
221
222 pub fn iter(&self) -> impl Iterator<Item = (UntypedAssetId, &ERA)> {
223 self.0.iter().map(|(k, v)| (*k, v))
224 }
225
226 pub fn iter_mut(&mut self) -> impl Iterator<Item = (UntypedAssetId, &mut ERA)> {
227 self.0.iter_mut().map(|(k, v)| (*k, v))
228 }
229}
230
231#[derive(Resource)]
232struct CachedExtractErasedRenderAssetSystemState<A: ErasedRenderAsset> {
233 state: SystemState<(
234 MessageReader<'static, 'static, AssetEvent<A::SourceAsset>>,
235 ResMut<'static, Assets<A::SourceAsset>>,
236 )>,
237}
238
239impl<A: ErasedRenderAsset> FromWorld for CachedExtractErasedRenderAssetSystemState<A> {
240 fn from_world(world: &mut bevy_ecs::world::World) -> Self {
241 Self {
242 state: SystemState::new(world),
243 }
244 }
245}
246
247#[derive(Resource)]
250pub(crate) struct ErasedRenderAssetsToReExtract<A: ErasedRenderAsset> {
251 ids: Vec<AssetId<A::SourceAsset>>,
252}
253
254fn collect_erased_render_assets_to_reextract<A: ErasedRenderAsset>(
256 mut commands: Commands,
257 mut render_assets: ResMut<ErasedRenderAssets<A::ErasedAsset>>,
258 mut prepare_next_frame: ResMut<PrepareNextFrameAssets<A>>,
259) {
260 let source_type_id = core::any::TypeId::of::<A::SourceAsset>();
261 let mut ids = Vec::new();
264 render_assets.0.retain(|untyped_id, _| {
265 if untyped_id.type_id() == source_type_id {
266 ids.push(untyped_id.typed());
267 false
268 } else {
269 true
270 }
271 });
272 prepare_next_frame.assets.clear();
273 if !ids.is_empty() {
274 commands.insert_resource(ErasedRenderAssetsToReExtract::<A> { ids });
275 }
276}
277
278pub(crate) fn extract_erased_render_asset<A: ErasedRenderAsset>(
281 mut to_reextract: Option<ResMut<ErasedRenderAssetsToReExtract<A>>>,
282 mut extracted_assets: ResMut<ExtractedAssets<A>>,
283 mut main_world: ResMut<MainWorld>,
284 mut needs_extracting: Local<HashSet<AssetId<A::SourceAsset>>>,
285) {
286 extracted_assets.extracted.clear();
287 extracted_assets.removed.clear();
288 extracted_assets.modified.clear();
289 extracted_assets.added.clear();
290 needs_extracting.clear();
291
292 let reextract_ids = to_reextract
293 .as_mut()
294 .map(|r| core::mem::take(&mut r.ids))
295 .filter(|ids| !ids.is_empty());
296
297 main_world.resource_scope(
298 |world, mut cached_state: Mut<CachedExtractErasedRenderAssetSystemState<A>>| {
299 let (mut events, mut assets) = cached_state.state.get_mut(world).unwrap();
300
301 if let Some(reextract_ids) = reextract_ids {
302 needs_extracting.extend(reextract_ids);
303 }
304
305 for event in events.read() {
306 #[expect(
307 clippy::match_same_arms,
308 reason = "LoadedWithDependencies is marked as a TODO, so it's likely this will no longer lint soon."
309 )]
310 match event {
311 AssetEvent::Added { id } => {
312 needs_extracting.insert(*id);
313 }
314 AssetEvent::Modified { id } => {
315 needs_extracting.insert(*id);
316 extracted_assets.modified.insert(*id);
317 }
318 AssetEvent::Removed { .. } => {
319 }
322 AssetEvent::Unused { id } => {
323 needs_extracting.remove(id);
324 extracted_assets.modified.remove(id);
325 extracted_assets.removed.insert(*id);
326 }
327 AssetEvent::LoadedWithDependencies { .. } => {
328 }
330 }
331 }
332
333 for id in needs_extracting.drain() {
334 if let Some(asset) = assets.get(id) {
335 let asset_usage = A::asset_usage(asset);
336 if asset_usage.contains(RenderAssetUsages::RENDER_WORLD) {
337 if asset_usage == RenderAssetUsages::RENDER_WORLD {
338 if let Some(asset) = assets.remove(id) {
339 extracted_assets.extracted.push((id, asset));
340 extracted_assets.added.insert(id);
341 }
342 } else {
343 extracted_assets.extracted.push((id, asset.clone()));
344 extracted_assets.added.insert(id);
345 }
346 }
347 }
348 }
349
350 cached_state.state.apply(world);
351 },
352 );
353}
354
355#[derive(Resource)]
358pub struct PrepareNextFrameAssets<A: ErasedRenderAsset> {
359 assets: Vec<(AssetId<A::SourceAsset>, A::SourceAsset)>,
360}
361
362impl<A: ErasedRenderAsset> Default for PrepareNextFrameAssets<A> {
363 fn default() -> Self {
364 Self {
365 assets: Default::default(),
366 }
367 }
368}
369
370pub fn prepare_erased_assets<A: ErasedRenderAsset>(
373 mut extracted_assets: ResMut<ExtractedAssets<A>>,
374 mut render_assets: ResMut<ErasedRenderAssets<A::ErasedAsset>>,
375 mut prepare_next_frame: ResMut<PrepareNextFrameAssets<A>>,
376 param: StaticSystemParam<<A as ErasedRenderAsset>::Param>,
377 bpf: Res<RenderAssetBytesPerFrameLimiter>,
378) {
379 let mut wrote_asset_count = 0;
380
381 let mut param = param.into_inner();
382 let queued_assets = core::mem::take(&mut prepare_next_frame.assets);
383 for (id, extracted_asset) in queued_assets {
384 if extracted_assets.removed.contains(&id) || extracted_assets.added.contains(&id) {
385 continue;
387 }
388
389 let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) {
390 if bpf.exhausted() {
395 prepare_next_frame.assets.push((id, extracted_asset));
396 continue;
397 }
398 size
399 } else {
400 0
401 };
402
403 match A::prepare_asset(extracted_asset, id, &mut param) {
404 Ok(prepared_asset) => {
405 render_assets.insert(id, prepared_asset);
406 bpf.write_bytes(write_bytes);
407 wrote_asset_count += 1;
408 }
409 Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => {
410 prepare_next_frame.assets.push((id, extracted_asset));
411 }
412 Err(PrepareAssetError::AsBindGroupError(e)) => {
413 error!(
414 "{} Bind group construction failed: {e}",
415 core::any::type_name::<A>()
416 );
417 }
418 }
419 }
420
421 for removed in extracted_assets.removed.drain() {
422 render_assets.remove(removed);
423 A::unload_asset(removed, &mut param);
424 }
425
426 for (id, extracted_asset) in extracted_assets.extracted.drain(..) {
427 render_assets.remove(id);
431
432 let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) {
433 if bpf.exhausted() {
434 prepare_next_frame.assets.push((id, extracted_asset));
435 continue;
436 }
437 size
438 } else {
439 0
440 };
441
442 match A::prepare_asset(extracted_asset, id, &mut param) {
443 Ok(prepared_asset) => {
444 render_assets.insert(id, prepared_asset);
445 bpf.write_bytes(write_bytes);
446 wrote_asset_count += 1;
447 }
448 Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => {
449 prepare_next_frame.assets.push((id, extracted_asset));
450 }
451 Err(PrepareAssetError::AsBindGroupError(e)) => {
452 error!(
453 "{} Bind group construction failed: {e}",
454 core::any::type_name::<A>()
455 );
456 }
457 }
458 }
459
460 if bpf.exhausted() && !prepare_next_frame.assets.is_empty() {
461 debug!(
462 "{} write budget exhausted with {} assets remaining (wrote {})",
463 core::any::type_name::<A>(),
464 prepare_next_frame.assets.len(),
465 wrote_asset_count
466 );
467 }
468}