-
-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Throttle render assets #12622
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Throttle render assets #12622
Changes from 7 commits
be68929
e6ebbfe
5974ef8
4008b77
3fc4241
f601e62
5f5119f
a8ece2d
a3bd114
7227a33
92a6890
339e96b
fa80135
5501346
cfb83ac
dc0a5bc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,8 @@ use bevy_ecs::{ | |
| world::{FromWorld, Mut}, | ||
| }; | ||
| use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; | ||
| use bevy_utils::{HashMap, HashSet}; | ||
| use bevy_render_macros::ExtractResource; | ||
| use bevy_utils::{tracing::debug, HashMap, HashSet}; | ||
| use serde::{Deserialize, Serialize}; | ||
| use std::marker::PhantomData; | ||
| use thiserror::Error; | ||
|
|
@@ -38,6 +39,12 @@ pub trait RenderAsset: Asset + Clone { | |
| /// Whether or not to unload the asset after extracting it to the render world. | ||
| fn asset_usage(&self) -> RenderAssetUsages; | ||
|
|
||
| /// Size of the data the asset will upload to the gpu. Specifying a return value | ||
| /// will allow the asset to be throttled via [`RenderAssetBytesPerFrame`]. | ||
| fn byte_len(&self) -> Option<usize> { | ||
robtfm marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| None | ||
| } | ||
|
|
||
| /// Prepares the asset for the GPU by transforming it into a [`RenderAsset::PreparedAsset`]. | ||
| /// | ||
| /// ECS data may be accessed via `param`. | ||
|
|
@@ -158,13 +165,15 @@ impl<A: RenderAsset> RenderAssetDependency for A { | |
| pub struct ExtractedAssets<A: RenderAsset> { | ||
| extracted: Vec<(AssetId<A>, A)>, | ||
| removed: Vec<AssetId<A>>, | ||
| added: Vec<AssetId<A>>, | ||
| } | ||
|
|
||
| impl<A: RenderAsset> Default for ExtractedAssets<A> { | ||
| fn default() -> Self { | ||
| Self { | ||
| extracted: Default::default(), | ||
| removed: Default::default(), | ||
| added: Default::default(), | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -254,16 +263,19 @@ fn extract_render_asset<A: RenderAsset>(mut commands: Commands, mut main_world: | |
| } | ||
|
|
||
| let mut extracted_assets = Vec::new(); | ||
| let mut added = Vec::new(); | ||
| for id in changed_assets.drain() { | ||
| if let Some(asset) = assets.get(id) { | ||
| let asset_usage = asset.asset_usage(); | ||
| if asset_usage.contains(RenderAssetUsages::RENDER_WORLD) { | ||
| if asset_usage == RenderAssetUsages::RENDER_WORLD { | ||
| if let Some(asset) = assets.remove(id) { | ||
| extracted_assets.push((id, asset)); | ||
| added.push(id); | ||
| } | ||
| } else { | ||
| extracted_assets.push((id, asset.clone())); | ||
| added.push(id); | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -272,6 +284,7 @@ fn extract_render_asset<A: RenderAsset>(mut commands: Commands, mut main_world: | |
| commands.insert_resource(ExtractedAssets { | ||
| extracted: extracted_assets, | ||
| removed, | ||
| added, | ||
| }); | ||
| cached_state.state.apply(world); | ||
| }, | ||
|
|
@@ -300,17 +313,28 @@ pub fn prepare_assets<A: RenderAsset>( | |
| mut render_assets: ResMut<RenderAssets<A>>, | ||
| mut prepare_next_frame: ResMut<PrepareNextFrameAssets<A>>, | ||
| param: StaticSystemParam<<A as RenderAsset>::Param>, | ||
| mut bpf: ResMut<RenderAssetBytesPerFrame>, | ||
| ) { | ||
| let mut wrote = 0; | ||
|
|
||
| let mut param = param.into_inner(); | ||
| let queued_assets = std::mem::take(&mut prepare_next_frame.assets); | ||
| for (id, extracted_asset) in queued_assets { | ||
| if extracted_assets.removed.contains(&id) { | ||
| let mut queued_assets = std::mem::take(&mut prepare_next_frame.assets).into_iter(); | ||
| for (id, extracted_asset) in queued_assets.by_ref() { | ||
| if extracted_assets.removed.contains(&id) || extracted_assets.added.contains(&id) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the purpose of skipping newly added assets here?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the retry loop for assets that were previously extracted and were not ready in the previous frame. If the asset has been added again, there are now 2 versions and we don’t want to prepare the old one. This was the source of the font bug I had where some characters would not render: The code in prepare_sprites clears its cache on asset change events, then checks for presence in RenderAssets to repopulate the cache. If we prepare the old version then the code in the sprite module will see it as ready and will cache the previous version, resulting in the old asset being permanently bound for some glyphs.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ahhh, makes sense. Could you add a comment about this? |
||
| continue; | ||
| } | ||
|
|
||
| if let Some(size) = extracted_asset.byte_len() { | ||
robtfm marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if bpf.write_bytes(size) == 0 { | ||
| prepare_next_frame.assets.push((id, extracted_asset)); | ||
| continue; | ||
| } | ||
| } | ||
|
|
||
| match extracted_asset.prepare_asset(&mut param) { | ||
| Ok(prepared_asset) => { | ||
| render_assets.insert(id, prepared_asset); | ||
| wrote += 1; | ||
| } | ||
| Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => { | ||
| prepare_next_frame.assets.push((id, extracted_asset)); | ||
|
|
@@ -322,14 +346,75 @@ pub fn prepare_assets<A: RenderAsset>( | |
| render_assets.remove(removed); | ||
| } | ||
|
|
||
| for (id, extracted_asset) in extracted_assets.extracted.drain(..) { | ||
| let mut extracted_assets = extracted_assets.extracted.drain(..); | ||
|
|
||
| for (id, extracted_asset) in extracted_assets.by_ref() { | ||
| if let Some(size) = extracted_asset.byte_len() { | ||
robtfm marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if bpf.write_bytes(size) == 0 { | ||
| prepare_next_frame.assets.push((id, extracted_asset)); | ||
| render_assets.remove(id); | ||
| continue; | ||
| } | ||
| } | ||
|
|
||
| match extracted_asset.prepare_asset(&mut param) { | ||
| Ok(prepared_asset) => { | ||
| render_assets.insert(id, prepared_asset); | ||
| wrote += 1; | ||
| } | ||
| Err(PrepareAssetError::RetryNextUpdate(extracted_asset)) => { | ||
| prepare_next_frame.assets.push((id, extracted_asset)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if bpf.exhausted() { | ||
robtfm marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| debug!( | ||
| "{} write budget exhausted with {} assets remaining (wrote {})", | ||
| std::any::type_name::<A>(), | ||
| prepare_next_frame.assets.len(), | ||
| wrote | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /// 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 struct RenderAssetBytesPerFrame { | ||
| pub max_bytes: Option<usize>, | ||
| 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 | ||
| /// 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, | ||
| } | ||
| } | ||
|
|
||
| pub fn reset(&mut self) { | ||
| self.available = self.max_bytes.unwrap_or(usize::MAX); | ||
| } | ||
|
|
||
| pub fn write_bytes(&mut self, bytes: usize) -> usize { | ||
| if self.max_bytes.is_none() { | ||
| return bytes; | ||
| } | ||
|
|
||
| let write_bytes = bytes.min(self.available); | ||
| self.available -= write_bytes; | ||
| write_bytes | ||
| } | ||
|
|
||
| pub fn exhausted(&self) -> bool { | ||
| self.available == 0 | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.