1use crate::{
2 render_resource::AsBindGroupError, Extract, ExtractSchedule, MainWorld, Render, RenderApp,
3 RenderSystems, Res,
4};
5use bevy_app::{App, Plugin, SubApp};
6use bevy_asset::{Asset, AssetEvent, AssetId, Assets, RenderAssetUsages};
7use bevy_ecs::{
8 prelude::{Commands, IntoScheduleConfigs, MessageReader, ResMut, Resource},
9 schedule::{ScheduleConfigs, SystemSet},
10 system::{ScheduleSystem, StaticSystemParam, SystemParam, SystemParamItem, SystemState},
11 world::{FromWorld, Mut},
12};
13use bevy_platform::collections::{HashMap, HashSet};
14use core::marker::PhantomData;
15use core::sync::atomic::{AtomicUsize, Ordering};
16use thiserror::Error;
17use tracing::{debug, error};
18
19#[derive(Debug, Error)]
20pub enum PrepareAssetError<E: Send + Sync + 'static> {
21 #[error("Failed to prepare asset")]
22 RetryNextUpdate(E),
23 #[error("Failed to build bind group: {0}")]
24 AsBindGroupError(AsBindGroupError),
25}
26
27#[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)]
29pub struct AssetExtractionSystems;
30
31#[derive(Debug, Error)]
33pub enum AssetExtractionError {
34 #[error("The asset has already been extracted")]
35 AlreadyExtracted,
36 #[error("The asset type does not support extraction. To clone the asset to the renderworld, use `RenderAssetUsages::default()`")]
37 NoExtractionImplementation,
38}
39
40pub trait RenderAsset: Send + Sync + 'static + Sized {
48 type SourceAsset: Asset + Clone;
50
51 type Param: SystemParam;
55
56 #[inline]
58 fn asset_usage(_source_asset: &Self::SourceAsset) -> RenderAssetUsages {
59 RenderAssetUsages::default()
60 }
61
62 #[inline]
65 #[expect(
66 unused_variables,
67 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."
68 )]
69 fn byte_len(source_asset: &Self::SourceAsset) -> Option<usize> {
70 None
71 }
72
73 fn prepare_asset(
77 source_asset: Self::SourceAsset,
78 asset_id: AssetId<Self::SourceAsset>,
79 param: &mut SystemParamItem<Self::Param>,
80 previous_asset: Option<&Self>,
81 ) -> Result<Self, PrepareAssetError<Self::SourceAsset>>;
82
83 fn unload_asset(
90 _source_asset: AssetId<Self::SourceAsset>,
91 _param: &mut SystemParamItem<Self::Param>,
92 ) {
93 }
94
95 fn take_gpu_data(
101 _source: &mut Self::SourceAsset,
102 _previous_gpu_asset: Option<&Self>,
103 ) -> Result<Self::SourceAsset, AssetExtractionError> {
104 Err(AssetExtractionError::NoExtractionImplementation)
105 }
106}
107
108pub struct RenderAssetPlugin<A: RenderAsset, AFTER: RenderAssetDependency + 'static = ()> {
119 phantom: PhantomData<fn() -> (A, AFTER)>,
120}
121
122impl<A: RenderAsset, AFTER: RenderAssetDependency + 'static> Default
123 for RenderAssetPlugin<A, AFTER>
124{
125 fn default() -> Self {
126 Self {
127 phantom: Default::default(),
128 }
129 }
130}
131
132impl<A: RenderAsset, AFTER: RenderAssetDependency + 'static> Plugin
133 for RenderAssetPlugin<A, AFTER>
134{
135 fn build(&self, app: &mut App) {
136 app.init_resource::<CachedExtractRenderAssetSystemState<A>>();
137 if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
138 render_app
139 .init_resource::<ExtractedAssets<A>>()
140 .init_resource::<RenderAssets<A>>()
141 .init_resource::<PrepareNextFrameAssets<A>>()
142 .add_systems(
143 ExtractSchedule,
144 extract_render_asset::<A>.in_set(AssetExtractionSystems),
145 );
146 AFTER::register_system(
147 render_app,
148 prepare_assets::<A>.in_set(RenderSystems::PrepareAssets),
149 );
150 }
151 }
152}
153
154pub trait RenderAssetDependency {
156 fn register_system(render_app: &mut SubApp, system: ScheduleConfigs<ScheduleSystem>);
157}
158
159impl RenderAssetDependency for () {
160 fn register_system(render_app: &mut SubApp, system: ScheduleConfigs<ScheduleSystem>) {
161 render_app.add_systems(Render, system);
162 }
163}
164
165impl<A: RenderAsset> RenderAssetDependency for A {
166 fn register_system(render_app: &mut SubApp, system: ScheduleConfigs<ScheduleSystem>) {
167 render_app.add_systems(Render, system.after(prepare_assets::<A>));
168 }
169}
170
171#[derive(Resource)]
173pub struct ExtractedAssets<A: RenderAsset> {
174 pub extracted: Vec<(AssetId<A::SourceAsset>, A::SourceAsset)>,
178
179 pub removed: HashSet<AssetId<A::SourceAsset>>,
183
184 pub modified: HashSet<AssetId<A::SourceAsset>>,
186
187 pub added: HashSet<AssetId<A::SourceAsset>>,
189}
190
191impl<A: RenderAsset> Default for ExtractedAssets<A> {
192 fn default() -> Self {
193 Self {
194 extracted: Default::default(),
195 removed: Default::default(),
196 modified: Default::default(),
197 added: Default::default(),
198 }
199 }
200}
201
202#[derive(Resource)]
205pub struct RenderAssets<A: RenderAsset>(HashMap<AssetId<A::SourceAsset>, A>);
206
207impl<A: RenderAsset> Default for RenderAssets<A> {
208 fn default() -> Self {
209 Self(Default::default())
210 }
211}
212
213impl<A: RenderAsset> RenderAssets<A> {
214 pub fn get(&self, id: impl Into<AssetId<A::SourceAsset>>) -> Option<&A> {
215 self.0.get(&id.into())
216 }
217
218 pub fn get_mut(&mut self, id: impl Into<AssetId<A::SourceAsset>>) -> Option<&mut A> {
219 self.0.get_mut(&id.into())
220 }
221
222 pub fn insert(&mut self, id: impl Into<AssetId<A::SourceAsset>>, value: A) -> Option<A> {
223 self.0.insert(id.into(), value)
224 }
225
226 pub fn remove(&mut self, id: impl Into<AssetId<A::SourceAsset>>) -> Option<A> {
227 self.0.remove(&id.into())
228 }
229
230 pub fn iter(&self) -> impl Iterator<Item = (AssetId<A::SourceAsset>, &A)> {
231 self.0.iter().map(|(k, v)| (*k, v))
232 }
233
234 pub fn iter_mut(&mut self) -> impl Iterator<Item = (AssetId<A::SourceAsset>, &mut A)> {
235 self.0.iter_mut().map(|(k, v)| (*k, v))
236 }
237}
238
239#[derive(Resource)]
240struct CachedExtractRenderAssetSystemState<A: RenderAsset> {
241 state: SystemState<(
242 MessageReader<'static, 'static, AssetEvent<A::SourceAsset>>,
243 ResMut<'static, Assets<A::SourceAsset>>,
244 Option<Res<'static, RenderAssets<A>>>,
245 )>,
246}
247
248impl<A: RenderAsset> FromWorld for CachedExtractRenderAssetSystemState<A> {
249 fn from_world(world: &mut bevy_ecs::world::World) -> Self {
250 Self {
251 state: SystemState::new(world),
252 }
253 }
254}
255
256pub(crate) fn extract_render_asset<A: RenderAsset>(
259 mut commands: Commands,
260 mut main_world: ResMut<MainWorld>,
261) {
262 main_world.resource_scope(
263 |world, mut cached_state: Mut<CachedExtractRenderAssetSystemState<A>>| {
264 let (mut events, mut assets, maybe_render_assets) = cached_state.state.get_mut(world);
265
266 let mut needs_extracting = <HashSet<_>>::default();
267 let mut removed = <HashSet<_>>::default();
268 let mut modified = <HashSet<_>>::default();
269
270 for event in events.read() {
271 #[expect(
272 clippy::match_same_arms,
273 reason = "LoadedWithDependencies is marked as a TODO, so it's likely this will no longer lint soon."
274 )]
275 match event {
276 AssetEvent::Added { id } => {
277 needs_extracting.insert(*id);
278 }
279 AssetEvent::Modified { id } => {
280 needs_extracting.insert(*id);
281 modified.insert(*id);
282 }
283 AssetEvent::Removed { .. } => {
284 }
287 AssetEvent::Unused { id } => {
288 needs_extracting.remove(id);
289 modified.remove(id);
290 removed.insert(*id);
291 }
292 AssetEvent::LoadedWithDependencies { .. } => {
293 }
295 }
296 }
297
298 let mut extracted_assets = Vec::new();
299 let mut added = <HashSet<_>>::default();
300 for id in needs_extracting.drain() {
301 if let Some(asset) = assets.get(id) {
302 let asset_usage = A::asset_usage(asset);
303 if asset_usage.contains(RenderAssetUsages::RENDER_WORLD) {
304 if asset_usage == RenderAssetUsages::RENDER_WORLD {
305 if let Some(asset) = assets.get_mut_untracked(id) {
306 let previous_asset = maybe_render_assets.as_ref().and_then(|render_assets| render_assets.get(id));
307 match A::take_gpu_data(asset, previous_asset) {
308 Ok(gpu_data_asset) => {
309 extracted_assets.push((id, gpu_data_asset));
310 added.insert(id);
311 }
312 Err(e) => {
313 error!("{} with RenderAssetUsages == RENDER_WORLD cannot be extracted: {e}", core::any::type_name::<A>());
314 }
315 };
316 }
317 } else {
318 extracted_assets.push((id, asset.clone()));
319 added.insert(id);
320 }
321 }
322 }
323 }
324
325 commands.insert_resource(ExtractedAssets::<A> {
326 extracted: extracted_assets,
327 removed,
328 modified,
329 added,
330 });
331 cached_state.state.apply(world);
332 },
333 );
334}
335
336#[derive(Resource)]
339pub struct PrepareNextFrameAssets<A: RenderAsset> {
340 assets: Vec<(AssetId<A::SourceAsset>, A::SourceAsset)>,
341}
342
343impl<A: RenderAsset> Default for PrepareNextFrameAssets<A> {
344 fn default() -> Self {
345 Self {
346 assets: Default::default(),
347 }
348 }
349}
350
351pub fn prepare_assets<A: RenderAsset>(
354 mut extracted_assets: ResMut<ExtractedAssets<A>>,
355 mut render_assets: ResMut<RenderAssets<A>>,
356 mut prepare_next_frame: ResMut<PrepareNextFrameAssets<A>>,
357 param: StaticSystemParam<<A as RenderAsset>::Param>,
358 bpf: Res<RenderAssetBytesPerFrameLimiter>,
359) {
360 let mut wrote_asset_count = 0;
361
362 let mut param = param.into_inner();
363 let queued_assets = core::mem::take(&mut prepare_next_frame.assets);
364 for (id, extracted_asset) in queued_assets {
365 if extracted_assets.removed.contains(&id) || extracted_assets.added.contains(&id) {
366 continue;
368 }
369
370 let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) {
371 if bpf.exhausted() {
376 prepare_next_frame.assets.push((id, extracted_asset));
377 continue;
378 }
379 size
380 } else {
381 0
382 };
383
384 let previous_asset = render_assets.get(id);
385 match A::prepare_asset(extracted_asset, id, &mut param, previous_asset) {
386 Ok(prepared_asset) => {
387 render_assets.insert(id, prepared_asset);
388 bpf.write_bytes(write_bytes);
389 wrote_asset_count += 1;
390 }
391 Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => {
392 prepare_next_frame.assets.push((id, extracted_asset));
393 }
394 Err(PrepareAssetError::AsBindGroupError(e)) => {
395 error!(
396 "{} Bind group construction failed: {e}",
397 core::any::type_name::<A>()
398 );
399 }
400 }
401 }
402
403 for removed in extracted_assets.removed.drain() {
404 render_assets.remove(removed);
405 A::unload_asset(removed, &mut param);
406 }
407
408 for (id, extracted_asset) in extracted_assets.extracted.drain(..) {
409 let previous_asset = render_assets.remove(id);
413
414 let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) {
415 if bpf.exhausted() {
416 prepare_next_frame.assets.push((id, extracted_asset));
417 continue;
418 }
419 size
420 } else {
421 0
422 };
423
424 match A::prepare_asset(extracted_asset, id, &mut param, previous_asset.as_ref()) {
425 Ok(prepared_asset) => {
426 render_assets.insert(id, prepared_asset);
427 bpf.write_bytes(write_bytes);
428 wrote_asset_count += 1;
429 }
430 Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => {
431 prepare_next_frame.assets.push((id, extracted_asset));
432 }
433 Err(PrepareAssetError::AsBindGroupError(e)) => {
434 error!(
435 "{} Bind group construction failed: {e}",
436 core::any::type_name::<A>()
437 );
438 }
439 }
440 }
441
442 if bpf.exhausted() && !prepare_next_frame.assets.is_empty() {
443 debug!(
444 "{} write budget exhausted with {} assets remaining (wrote {})",
445 core::any::type_name::<A>(),
446 prepare_next_frame.assets.len(),
447 wrote_asset_count
448 );
449 }
450}
451
452pub fn reset_render_asset_bytes_per_frame(
453 mut bpf_limiter: ResMut<RenderAssetBytesPerFrameLimiter>,
454) {
455 bpf_limiter.reset();
456}
457
458pub fn extract_render_asset_bytes_per_frame(
459 bpf: Extract<Res<RenderAssetBytesPerFrame>>,
460 mut bpf_limiter: ResMut<RenderAssetBytesPerFrameLimiter>,
461) {
462 bpf_limiter.max_bytes = bpf.max_bytes;
463}
464
465#[derive(Resource, Default)]
469pub struct RenderAssetBytesPerFrame {
470 pub max_bytes: Option<usize>,
471}
472
473impl RenderAssetBytesPerFrame {
474 pub fn new(max_bytes: usize) -> Self {
482 Self {
483 max_bytes: Some(max_bytes),
484 }
485 }
486}
487
488#[derive(Resource, Default)]
492pub struct RenderAssetBytesPerFrameLimiter {
493 pub max_bytes: Option<usize>,
495 pub bytes_written: AtomicUsize,
497}
498
499impl RenderAssetBytesPerFrameLimiter {
500 pub fn reset(&mut self) {
502 if self.max_bytes.is_none() {
503 return;
504 }
505 self.bytes_written.store(0, Ordering::Relaxed);
506 }
507
508 pub fn available_bytes(&self, required_bytes: usize) -> usize {
510 if let Some(max_bytes) = self.max_bytes {
511 let total_bytes = self
512 .bytes_written
513 .fetch_add(required_bytes, Ordering::Relaxed);
514
515 if total_bytes >= max_bytes {
517 required_bytes.saturating_sub(total_bytes - max_bytes)
518 } else {
519 required_bytes
520 }
521 } else {
522 required_bytes
523 }
524 }
525
526 pub(crate) fn write_bytes(&self, bytes: usize) {
528 if self.max_bytes.is_some() && bytes > 0 {
529 self.bytes_written.fetch_add(bytes, Ordering::Relaxed);
530 }
531 }
532
533 pub(crate) fn exhausted(&self) -> bool {
535 if let Some(max_bytes) = self.max_bytes {
536 let bytes_written = self.bytes_written.load(Ordering::Relaxed);
537 bytes_written >= max_bytes
538 } else {
539 false
540 }
541 }
542}