diff --git a/crates/bevy_pbr/src/volumetric_fog/render.rs b/crates/bevy_pbr/src/volumetric_fog/render.rs index 632a6f2c30..07012a72e2 100644 --- a/crates/bevy_pbr/src/volumetric_fog/render.rs +++ b/crates/bevy_pbr/src/volumetric_fog/render.rs @@ -628,7 +628,10 @@ pub fn prepare_volumetric_fog_pipelines( >, meshes: Res>, ) { - let plane_mesh = meshes.get(&PLANE_MESH).expect("Plane mesh not found!"); + let Some(plane_mesh) = meshes.get(&PLANE_MESH) else { + // There's an off chance that the mesh won't be prepared yet if `RenderAssetBytesPerFrame` limiting is in use. + return; + }; for ( entity, diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index fcb8e62460..a75a7eb87c 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -78,9 +78,11 @@ pub use extract_param::Extract; use bevy_window::{PrimaryWindow, RawHandleWrapperHolder}; use experimental::occlusion_culling::OcclusionCullingPlugin; -use extract_resource::ExtractResourcePlugin; use globals::GlobalsPlugin; -use render_asset::RenderAssetBytesPerFrame; +use render_asset::{ + extract_render_asset_bytes_per_frame, reset_render_asset_bytes_per_frame, + RenderAssetBytesPerFrame, RenderAssetBytesPerFrameLimiter, +}; use renderer::{RenderAdapter, RenderDevice, RenderQueue}; use settings::RenderResources; use sync_world::{ @@ -408,8 +410,16 @@ impl Plugin for RenderPlugin { OcclusionCullingPlugin, )); - app.init_resource::() - .add_plugins(ExtractResourcePlugin::::default()); + app.init_resource::(); + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.init_resource::(); + render_app + .add_systems(ExtractSchedule, extract_render_asset_bytes_per_frame) + .add_systems( + Render, + reset_render_asset_bytes_per_frame.in_set(RenderSet::Cleanup), + ); + } app.register_type::() // These types cannot be registered in bevy_color, as it does not depend on the rest of Bevy @@ -465,14 +475,7 @@ impl Plugin for RenderPlugin { .insert_resource(device) .insert_resource(queue) .insert_resource(render_adapter) - .insert_resource(adapter_info) - .add_systems( - Render, - (|mut bpf: ResMut| { - bpf.reset(); - }) - .in_set(RenderSet::Cleanup), - ); + .insert_resource(adapter_info); } } } diff --git a/crates/bevy_render/src/render_asset.rs b/crates/bevy_render/src/render_asset.rs index 043b3554a1..03aa3860e2 100644 --- a/crates/bevy_render/src/render_asset.rs +++ b/crates/bevy_render/src/render_asset.rs @@ -1,18 +1,19 @@ use crate::{ - render_resource::AsBindGroupError, ExtractSchedule, MainWorld, Render, RenderApp, RenderSet, + render_resource::AsBindGroupError, Extract, ExtractSchedule, MainWorld, Render, RenderApp, + RenderSet, }; use bevy_app::{App, Plugin, SubApp}; pub use bevy_asset::RenderAssetUsages; use bevy_asset::{Asset, AssetEvent, AssetId, Assets}; use bevy_ecs::{ - prelude::{Commands, EventReader, IntoSystemConfigs, ResMut, Resource}, + prelude::{Commands, EventReader, IntoSystemConfigs, Res, ResMut, Resource}, schedule::{SystemConfigs, SystemSet}, system::{StaticSystemParam, SystemParam, SystemParamItem, SystemState}, world::{FromWorld, Mut}, }; use bevy_platform_support::collections::{HashMap, HashSet}; -use bevy_render_macros::ExtractResource; use core::marker::PhantomData; +use core::sync::atomic::{AtomicUsize, Ordering}; use thiserror::Error; use tracing::{debug, error}; @@ -308,7 +309,7 @@ pub fn prepare_assets( mut render_assets: ResMut>, mut prepare_next_frame: ResMut>, param: StaticSystemParam<::Param>, - mut bpf: ResMut, + bpf: Res, ) { let mut wrote_asset_count = 0; @@ -401,54 +402,94 @@ pub fn prepare_assets( } } -/// A resource that attempts to limit the amount of data transferred from cpu to gpu -/// each frame, preventing choppy frames at the cost of waiting longer for gpu assets -/// to become available -#[derive(Resource, Default, Debug, Clone, Copy, ExtractResource)] +pub fn reset_render_asset_bytes_per_frame( + mut bpf_limiter: ResMut, +) { + bpf_limiter.reset(); +} + +pub fn extract_render_asset_bytes_per_frame( + bpf: Extract>, + mut bpf_limiter: ResMut, +) { + bpf_limiter.max_bytes = bpf.max_bytes; +} + +/// A resource that defines the amount of data allowed to be transferred from CPU to GPU +/// each frame, preventing choppy frames at the cost of waiting longer for GPU assets +/// to become available. +#[derive(Resource, Default)] pub struct RenderAssetBytesPerFrame { pub max_bytes: Option, - pub available: usize, } impl RenderAssetBytesPerFrame { /// `max_bytes`: the number of bytes to write per frame. - /// this is a soft limit: only full assets are written currently, uploading stops + /// + /// This is a soft limit: only full assets are written currently, uploading stops /// after the first asset that exceeds the limit. + /// /// To participate, assets should implement [`RenderAsset::byte_len`]. If the default /// is not overridden, the assets are assumed to be small enough to upload without restriction. pub fn new(max_bytes: usize) -> Self { Self { max_bytes: Some(max_bytes), - available: 0, } } +} - /// Reset the available bytes. Called once per frame by the [`crate::RenderPlugin`]. +/// A render-world resource that facilitates limiting the data transferred from CPU to GPU +/// each frame, preventing choppy frames at the cost of waiting longer for GPU assets +/// to become available. +#[derive(Resource, Default)] +pub struct RenderAssetBytesPerFrameLimiter { + /// Populated by [`RenderAssetBytesPerFrame`] during extraction. + pub max_bytes: Option, + /// Bytes written this frame. + pub bytes_written: AtomicUsize, +} + +impl RenderAssetBytesPerFrameLimiter { + /// Reset the available bytes. Called once per frame during extraction by [`crate::RenderPlugin`]. pub fn reset(&mut self) { - self.available = self.max_bytes.unwrap_or(usize::MAX); - } - - /// check how many bytes are available since the last reset - pub fn available_bytes(&self, required_bytes: usize) -> usize { - if self.max_bytes.is_none() { - return required_bytes; - } - - required_bytes.min(self.available) - } - - /// decrease the available bytes for the current frame - fn write_bytes(&mut self, bytes: usize) { if self.max_bytes.is_none() { return; } - - let write_bytes = bytes.min(self.available); - self.available -= write_bytes; + self.bytes_written.store(0, Ordering::Relaxed); } - // check if any bytes remain available for writing this frame + /// Check how many bytes are available for writing. + pub fn available_bytes(&self, required_bytes: usize) -> usize { + if let Some(max_bytes) = self.max_bytes { + let total_bytes = self + .bytes_written + .fetch_add(required_bytes, Ordering::Relaxed); + + // The bytes available is the inverse of the amount we overshot max_bytes + if total_bytes >= max_bytes { + required_bytes.saturating_sub(total_bytes - max_bytes) + } else { + required_bytes + } + } else { + required_bytes + } + } + + /// Decreases the available bytes for the current frame. + fn write_bytes(&self, bytes: usize) { + if self.max_bytes.is_some() && bytes > 0 { + self.bytes_written.fetch_add(bytes, Ordering::Relaxed); + } + } + + /// Returns `true` if there are no remaining bytes available for writing this frame. fn exhausted(&self) -> bool { - self.max_bytes.is_some() && self.available == 0 + if let Some(max_bytes) = self.max_bytes { + let bytes_written = self.bytes_written.load(Ordering::Relaxed); + bytes_written >= max_bytes + } else { + false + } } }