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#[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 .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
141pub 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#[derive(Resource)]
160pub struct ExtractedAssets<A: ErasedRenderAsset> {
161 pub extracted: Vec<(AssetId<A::SourceAsset>, A::SourceAsset)>,
165
166 pub removed: HashSet<AssetId<A::SourceAsset>>,
170
171 pub modified: HashSet<AssetId<A::SourceAsset>>,
173
174 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#[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
242pub(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 }
273 AssetEvent::Unused { id } => {
274 needs_extracting.remove(id);
275 modified.remove(id);
276 removed.insert(*id);
277 }
278 AssetEvent::LoadedWithDependencies { .. } => {
279 }
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#[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
329pub 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 continue;
346 }
347
348 let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) {
349 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 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}