Skip to content

Commit dacb77d

Browse files
authored
Parallelize prepare_assets::<T> systems (#17914)
# Objective Because `prepare_assets::<T>` had a mutable reference to the `RenderAssetBytesPerFrame` resource, no render asset preparation could happen in parallel. This PR fixes this by using an `AtomicUsize` to count bytes written (if there's a limit in place), so that the system doesn't need mutable access. - Related: #12622 **Before** <img width="1049" alt="Screenshot 2025-02-17 at 11 40 53 AM" src="https://github.com/user-attachments/assets/040e6184-1192-4368-9597-5ceda4b8251b" /> **After** <img width="836" alt="Screenshot 2025-02-17 at 1 38 37 PM" src="https://github.com/user-attachments/assets/95488796-3323-425c-b0a6-4cf17753512e" /> ## Testing - Tested on a local project (with and without limiting enabled) - Someone with more knowledge of wgpu/underlying driver guts should confirm that this doesn't actually bite us by introducing contention (i.e. if buffer writing really *should be* serial).
1 parent 2a2e0a8 commit dacb77d

File tree

3 files changed

+87
-40
lines changed

3 files changed

+87
-40
lines changed

crates/bevy_pbr/src/volumetric_fog/render.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -628,7 +628,10 @@ pub fn prepare_volumetric_fog_pipelines(
628628
>,
629629
meshes: Res<RenderAssets<RenderMesh>>,
630630
) {
631-
let plane_mesh = meshes.get(&PLANE_MESH).expect("Plane mesh not found!");
631+
let Some(plane_mesh) = meshes.get(&PLANE_MESH) else {
632+
// There's an off chance that the mesh won't be prepared yet if `RenderAssetBytesPerFrame` limiting is in use.
633+
return;
634+
};
632635

633636
for (
634637
entity,

crates/bevy_render/src/lib.rs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,11 @@ pub use extract_param::Extract;
7878

7979
use bevy_window::{PrimaryWindow, RawHandleWrapperHolder};
8080
use experimental::occlusion_culling::OcclusionCullingPlugin;
81-
use extract_resource::ExtractResourcePlugin;
8281
use globals::GlobalsPlugin;
83-
use render_asset::RenderAssetBytesPerFrame;
82+
use render_asset::{
83+
extract_render_asset_bytes_per_frame, reset_render_asset_bytes_per_frame,
84+
RenderAssetBytesPerFrame, RenderAssetBytesPerFrameLimiter,
85+
};
8486
use renderer::{RenderAdapter, RenderDevice, RenderQueue};
8587
use settings::RenderResources;
8688
use sync_world::{
@@ -408,8 +410,16 @@ impl Plugin for RenderPlugin {
408410
OcclusionCullingPlugin,
409411
));
410412

411-
app.init_resource::<RenderAssetBytesPerFrame>()
412-
.add_plugins(ExtractResourcePlugin::<RenderAssetBytesPerFrame>::default());
413+
app.init_resource::<RenderAssetBytesPerFrame>();
414+
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
415+
render_app.init_resource::<RenderAssetBytesPerFrameLimiter>();
416+
render_app
417+
.add_systems(ExtractSchedule, extract_render_asset_bytes_per_frame)
418+
.add_systems(
419+
Render,
420+
reset_render_asset_bytes_per_frame.in_set(RenderSet::Cleanup),
421+
);
422+
}
413423

414424
app.register_type::<alpha::AlphaMode>()
415425
// 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 {
465475
.insert_resource(device)
466476
.insert_resource(queue)
467477
.insert_resource(render_adapter)
468-
.insert_resource(adapter_info)
469-
.add_systems(
470-
Render,
471-
(|mut bpf: ResMut<RenderAssetBytesPerFrame>| {
472-
bpf.reset();
473-
})
474-
.in_set(RenderSet::Cleanup),
475-
);
478+
.insert_resource(adapter_info);
476479
}
477480
}
478481
}

crates/bevy_render/src/render_asset.rs

Lines changed: 68 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
use crate::{
2-
render_resource::AsBindGroupError, ExtractSchedule, MainWorld, Render, RenderApp, RenderSet,
2+
render_resource::AsBindGroupError, Extract, ExtractSchedule, MainWorld, Render, RenderApp,
3+
RenderSet,
34
};
45
use bevy_app::{App, Plugin, SubApp};
56
pub use bevy_asset::RenderAssetUsages;
67
use bevy_asset::{Asset, AssetEvent, AssetId, Assets};
78
use bevy_ecs::{
8-
prelude::{Commands, EventReader, IntoSystemConfigs, ResMut, Resource},
9+
prelude::{Commands, EventReader, IntoSystemConfigs, Res, ResMut, Resource},
910
schedule::{SystemConfigs, SystemSet},
1011
system::{StaticSystemParam, SystemParam, SystemParamItem, SystemState},
1112
world::{FromWorld, Mut},
1213
};
1314
use bevy_platform_support::collections::{HashMap, HashSet};
14-
use bevy_render_macros::ExtractResource;
1515
use core::marker::PhantomData;
16+
use core::sync::atomic::{AtomicUsize, Ordering};
1617
use thiserror::Error;
1718
use tracing::{debug, error};
1819

@@ -308,7 +309,7 @@ pub fn prepare_assets<A: RenderAsset>(
308309
mut render_assets: ResMut<RenderAssets<A>>,
309310
mut prepare_next_frame: ResMut<PrepareNextFrameAssets<A>>,
310311
param: StaticSystemParam<<A as RenderAsset>::Param>,
311-
mut bpf: ResMut<RenderAssetBytesPerFrame>,
312+
bpf: Res<RenderAssetBytesPerFrameLimiter>,
312313
) {
313314
let mut wrote_asset_count = 0;
314315

@@ -401,54 +402,94 @@ pub fn prepare_assets<A: RenderAsset>(
401402
}
402403
}
403404

404-
/// A resource that attempts to limit the amount of data transferred from cpu to gpu
405-
/// each frame, preventing choppy frames at the cost of waiting longer for gpu assets
406-
/// to become available
407-
#[derive(Resource, Default, Debug, Clone, Copy, ExtractResource)]
405+
pub fn reset_render_asset_bytes_per_frame(
406+
mut bpf_limiter: ResMut<RenderAssetBytesPerFrameLimiter>,
407+
) {
408+
bpf_limiter.reset();
409+
}
410+
411+
pub fn extract_render_asset_bytes_per_frame(
412+
bpf: Extract<Res<RenderAssetBytesPerFrame>>,
413+
mut bpf_limiter: ResMut<RenderAssetBytesPerFrameLimiter>,
414+
) {
415+
bpf_limiter.max_bytes = bpf.max_bytes;
416+
}
417+
418+
/// A resource that defines the amount of data allowed to be transferred from CPU to GPU
419+
/// each frame, preventing choppy frames at the cost of waiting longer for GPU assets
420+
/// to become available.
421+
#[derive(Resource, Default)]
408422
pub struct RenderAssetBytesPerFrame {
409423
pub max_bytes: Option<usize>,
410-
pub available: usize,
411424
}
412425

413426
impl RenderAssetBytesPerFrame {
414427
/// `max_bytes`: the number of bytes to write per frame.
415-
/// this is a soft limit: only full assets are written currently, uploading stops
428+
///
429+
/// This is a soft limit: only full assets are written currently, uploading stops
416430
/// after the first asset that exceeds the limit.
431+
///
417432
/// To participate, assets should implement [`RenderAsset::byte_len`]. If the default
418433
/// is not overridden, the assets are assumed to be small enough to upload without restriction.
419434
pub fn new(max_bytes: usize) -> Self {
420435
Self {
421436
max_bytes: Some(max_bytes),
422-
available: 0,
423437
}
424438
}
439+
}
440+
441+
/// A render-world resource that facilitates limiting the data transferred from CPU to GPU
442+
/// each frame, preventing choppy frames at the cost of waiting longer for GPU assets
443+
/// to become available.
444+
#[derive(Resource, Default)]
445+
pub struct RenderAssetBytesPerFrameLimiter {
446+
/// Populated by [`RenderAssetBytesPerFrame`] during extraction.
447+
pub max_bytes: Option<usize>,
448+
/// Bytes written this frame.
449+
pub bytes_written: AtomicUsize,
450+
}
425451

426-
/// Reset the available bytes. Called once per frame by the [`crate::RenderPlugin`].
452+
impl RenderAssetBytesPerFrameLimiter {
453+
/// Reset the available bytes. Called once per frame during extraction by [`crate::RenderPlugin`].
427454
pub fn reset(&mut self) {
428-
self.available = self.max_bytes.unwrap_or(usize::MAX);
455+
if self.max_bytes.is_none() {
456+
return;
457+
}
458+
self.bytes_written.store(0, Ordering::Relaxed);
429459
}
430460

431-
/// check how many bytes are available since the last reset
461+
/// Check how many bytes are available for writing.
432462
pub fn available_bytes(&self, required_bytes: usize) -> usize {
433-
if self.max_bytes.is_none() {
434-
return required_bytes;
463+
if let Some(max_bytes) = self.max_bytes {
464+
let total_bytes = self
465+
.bytes_written
466+
.fetch_add(required_bytes, Ordering::Relaxed);
467+
468+
// The bytes available is the inverse of the amount we overshot max_bytes
469+
if total_bytes >= max_bytes {
470+
required_bytes.saturating_sub(total_bytes - max_bytes)
471+
} else {
472+
required_bytes
473+
}
474+
} else {
475+
required_bytes
435476
}
436-
437-
required_bytes.min(self.available)
438477
}
439478

440-
/// decrease the available bytes for the current frame
441-
fn write_bytes(&mut self, bytes: usize) {
442-
if self.max_bytes.is_none() {
443-
return;
479+
/// Decreases the available bytes for the current frame.
480+
fn write_bytes(&self, bytes: usize) {
481+
if self.max_bytes.is_some() && bytes > 0 {
482+
self.bytes_written.fetch_add(bytes, Ordering::Relaxed);
444483
}
445-
446-
let write_bytes = bytes.min(self.available);
447-
self.available -= write_bytes;
448484
}
449485

450-
// check if any bytes remain available for writing this frame
486+
/// Returns `true` if there are no remaining bytes available for writing this frame.
451487
fn exhausted(&self) -> bool {
452-
self.max_bytes.is_some() && self.available == 0
488+
if let Some(max_bytes) = self.max_bytes {
489+
let bytes_written = self.bytes_written.load(Ordering::Relaxed);
490+
bytes_written >= max_bytes
491+
} else {
492+
false
493+
}
453494
}
454495
}

0 commit comments

Comments
 (0)